Commit 40d432f3 authored by Nacim Goura's avatar Nacim Goura

add autocomplete, synonym and fix bug

parent 29374670
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
# base package # base package
meteor-base@1.1.0 # Packages every Meteor app needs to have meteor-base@1.1.0 # Packages every Meteor app needs to have
mobile-experience@1.0.4 # Packages for a great mobile UX mobile-experience@1.0.4 # Packages for a great mobile UX
mongo@1.1.19 # The database Meteor supports right now mongo # The database Meteor supports right now
blaze-html-templates # Compile .html files into Meteor Blaze views blaze-html-templates # Compile .html files into Meteor Blaze views
reactive-var@1.0.11 # Reactive variable for tracker reactive-var@1.0.11 # Reactive variable for tracker
tracker@1.1.3 # Meteor's client-side reactive programming library tracker@1.1.3 # Meteor's client-side reactive programming library
......
...@@ -67,10 +67,10 @@ mobile-status-bar@1.0.14 ...@@ -67,10 +67,10 @@ mobile-status-bar@1.0.14
modules@0.9.2 modules@0.9.2
modules-runtime@0.8.0 modules-runtime@0.8.0
momentjs:moment@2.18.1 momentjs:moment@2.18.1
mongo@1.1.19 mongo@1.1.22
mongo-id@1.0.6 mongo-id@1.0.6
npm-bcrypt@0.9.3 npm-bcrypt@0.9.3
npm-mongo@2.2.24 npm-mongo@2.2.30
observe-sequence@1.0.16 observe-sequence@1.0.16
ordered-dict@1.0.9 ordered-dict@1.0.9
ostrio:cookies@2.2.2 ostrio:cookies@2.2.2
......
...@@ -15,3 +15,4 @@ ...@@ -15,3 +15,4 @@
@import "../imports/ui/stylesheets/izitoast"; @import "../imports/ui/stylesheets/izitoast";
@import "../imports/ui/stylesheets/sweetalert2"; @import "../imports/ui/stylesheets/sweetalert2";
@import "../imports/ui/stylesheets/autocomplete"; @import "../imports/ui/stylesheets/autocomplete";
@import "../imports/ui/stylesheets/resultSearch";
...@@ -5,6 +5,7 @@ import { Accounts } from 'meteor/accounts-base'; ...@@ -5,6 +5,7 @@ import { Accounts } from 'meteor/accounts-base';
import { Roles } from 'meteor/alanning:roles'; import { Roles } from 'meteor/alanning:roles';
import formAccountSchema from '/imports/api/account/formAccountSchema'; import formAccountSchema from '/imports/api/account/formAccountSchema';
import { defineConfig } from '/imports/api/config/methods'; import { defineConfig } from '/imports/api/config/methods';
import Search from '/imports/api/search/server/search';
/** /**
* add user account * add user account
...@@ -34,4 +35,15 @@ export function deleteAccount(id) { ...@@ -34,4 +35,15 @@ export function deleteAccount(id) {
Meteor.methods({ Meteor.methods({
addAccount, addAccount,
deleteAccount, deleteAccount,
testCharge: () => {
// pour la test de charge
const search = new Search('l6g4zuw2vsbegqzw7');
return search.searchWebsite('anap')
.then((result) => {
console.log(result);
return JSON.stringify(result);
}).catch((error) => {
console.log(error);
});
},
}); });
...@@ -147,7 +147,8 @@ export default class crawlWebsite extends CrawlGeneric { ...@@ -147,7 +147,8 @@ export default class crawlWebsite extends CrawlGeneric {
${$('meta[property="og:title"]').attr('content')} ${$('meta[property="og:title"]').attr('content')}
${$('meta[name=title]').attr('content')} ${$('meta[name=title]').attr('content')}
${$('meta[property="video:actor"]').attr('content')} ${$('meta[property="video:actor"]').attr('content')}
${$('meta[property="video:director"]').attr('content')} `; ${$('meta[property="video:director"]').attr('content')}
${$('meta[property="og:description"]').attr('content')} `;
const dataForIndex = { const dataForIndex = {
tag: 'website', tag: 'website',
jobName: this.name, jobName: this.name,
...@@ -155,10 +156,11 @@ export default class crawlWebsite extends CrawlGeneric { ...@@ -155,10 +156,11 @@ export default class crawlWebsite extends CrawlGeneric {
title, title,
title_suggest: { title_suggest: {
// replace - and _ and multiple space for autocompletion // replace - and _ and multiple space for autocompletion
input: title.replace(/[-_]/g, ' ').replace(/[^\S]{2,}/, ' ').trim(), input: title.replace(/[-_]/g, ' ').replace(/[^\S]{2,}/g, ' ').trim(),
}, },
description: checkData.cleanText(description), description: checkData.cleanText(description),
body: checkData.cleanText(body.text()), body: checkData.cleanText(body.text()),
image: $('meta[property="og:image"]').attr('content'),
html: body.html(), html: body.html(),
urlText: checkData.cleanText(decodeURI(currentUrl)).replace(/http|www|html/g, '').replace(/\.|-/g, ' '), urlText: checkData.cleanText(decodeURI(currentUrl)).replace(/http|www|html/g, '').replace(/\.|-/g, ' '),
url: decodeURI(currentUrl), url: decodeURI(currentUrl),
......
...@@ -7,5 +7,8 @@ export default new SimpleSchema({ ...@@ -7,5 +7,8 @@ export default new SimpleSchema({
searchTerm: { searchTerm: {
label: false, label: false,
type: String, type: String,
autoform: {
type: 'search',
},
}, },
}, { tracker: Tracker }); }, { tracker: Tracker });
...@@ -12,6 +12,7 @@ export async function searchWebsite(data, userId) { ...@@ -12,6 +12,7 @@ export async function searchWebsite(data, userId) {
try { try {
const results = await search.searchWebsite(data.searchTerm); const results = await search.searchWebsite(data.searchTerm);
return { return {
time: results.took ? results.took / 1000 : null,
total: results.hits.total, total: results.hits.total,
list: _.map(results.hits.hits, '_source'), list: _.map(results.hits.hits, '_source'),
}; };
......
...@@ -27,7 +27,7 @@ export default class Search { ...@@ -27,7 +27,7 @@ export default class Search {
* common ( sépare les tokens les plus présents dans l’index des autres, et ne les utilise que pour améliorer la pertinence ) * common ( sépare les tokens les plus présents dans l’index des autres, et ne les utilise que pour améliorer la pertinence )
* fuzziness (permet une recherche même avec des fautes) * fuzziness (permet une recherche même avec des fautes)
*/ */
params._source = ["title", "url"]; params._source = ["title", "url", "description", "image"];
params.query = { params.query = {
"bool": { "bool": {
"must_not": [ "must_not": [
......
...@@ -15,6 +15,10 @@ exports.analyser = { ...@@ -15,6 +15,10 @@ exports.analyser = {
], ],
}, },
// synonyme // synonyme
/**
* Attention après ajout de synonyme, réindexer
* https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-synonym-tokenfilter.html
*/
french_synonym: { french_synonym: {
type: 'synonym', type: 'synonym',
ignore_case: true, ignore_case: true,
...@@ -25,8 +29,43 @@ exports.analyser = { ...@@ -25,8 +29,43 @@ exports.analyser = {
'anap, Agence nationale d\'appui à la performance des établissements de santé et médico-sociaux', 'anap, Agence nationale d\'appui à la performance des établissements de santé et médico-sociaux',
'ars, agences régionales de santé', 'ars, agences régionales de santé',
'c dans l\'oxygene, c dans l\'air', 'c dans l\'oxygene, c dans l\'air',
'+, plus', '+ => plus',
'%, pour cent', '% => pour cent',
'10% => 10 pour cent, 10 pourcent,dix pour cent, dix pourcent',
'1 => un',
'2 => deux',
'3 => trois',
'4 => quatre',
'5 => cinq',
'6 => six',
'7 => sept',
'8 => huit',
'9 => neuf',
'10 => dix',
'11 => onze',
'12 => douze',
'13 => treize',
'14 => quatorze',
'15 => quinze',
'16 => seize',
'17 => dix-sept,dix sept',
'18 => dix-huit,dix huit',
'19 => dix-neuf,dix neuf',
'20 => vingt',
'min, minute, minimum',
'boulevard, rue, avenue',
'ville, village',
'cosmos, galaxie,univers',
'docteur, medecin, doctor',
'foot2rue, foot de rue,foot 2 rue',
'animaux, betes',
'chine, asie, asiatique, chinois, cantonais, jaune',
'accusé, coupable',
'sdf, sans domilcile fixe',
'Histoire, légende',
'tgv, train, ter, sncf, train grande vitesse',
'canada,canadienne',
'terrien,terre',
], ],
}, },
// radical des mots // radical des mots
...@@ -164,6 +203,10 @@ exports.mapping = { ...@@ -164,6 +203,10 @@ exports.mapping = {
}, },
}, },
}, },
image: {
type: 'text',
analyzer: 'standard',
},
h1: { h1: {
type: 'text', type: 'text',
analyzer: 'french_light', analyzer: 'french_light',
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import 'bootstrap-sass'; import 'bootstrap-sass';
import moment from 'moment'; import moment from 'moment';
import 'moment/locale/fr'; import 'moment/locale/fr';
import './routes'; import './routes';
moment.locale('fr'); moment.locale('fr');
...@@ -8,3 +8,4 @@ import './register-api'; ...@@ -8,3 +8,4 @@ import './register-api';
// task of cron // task of cron
import './cron'; import './cron';
<template name="listResultSearch"> <template name="listResultSearchTpl">
<div class="panel panel-default wrapper"> <div class="panel panel-default wrapper">
<div class="panel-body"> <div class="panel-body">
...@@ -7,7 +7,11 @@ ...@@ -7,7 +7,11 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading text-center"> <div class="panel-heading text-center">
<h3 class="panel-title">Résultat de site</h3> <h3 class="panel-title">Résultats de site
{{#if websiteResults.time}}
<span class="small">(trouvés en {{websiteResults.time}} secondes)</span>
{{/if}}
</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{{#each result in websiteResults.list}} {{#each result in websiteResults.list}}
...@@ -24,7 +28,7 @@ ...@@ -24,7 +28,7 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading text-center"> <div class="panel-heading text-center">
<h3 class="panel-title">Résultat de réseaux sociaux</h3> <h3 class="panel-title">Résultats de réseaux sociaux</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{{#each result in apiResults.list}} {{#each result in apiResults.list}}
...@@ -38,7 +42,7 @@ ...@@ -38,7 +42,7 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading text-center"> <div class="panel-heading text-center">
<h3 class="panel-title">Résultat de fichiers</h3> <h3 class="panel-title">Résultats de fichiers</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{{#each result in documentResults.list}} {{#each result in documentResults.list}}
......
import { Template } from 'meteor/templating';
import './list.html'; import './list.html';
Template.listResultSearch.helpers({ Template.listResultSearchTpl.helpers({
websiteResults: () => Session.get('websiteResults'), websiteResults: () => Session.get('websiteResults'),
apiResults: () => Session.get('apiResults'), apiResults: () => Session.get('apiResults'),
documentResults: () => Session.get('documentResults'), documentResults: () => Session.get('documentResults'),
......
...@@ -5,6 +5,7 @@ import TabsCollection from '/imports/api/tabs/tabsCollection'; ...@@ -5,6 +5,7 @@ import TabsCollection from '/imports/api/tabs/tabsCollection';
import '/imports/ui/components/tabs/tabs'; import '/imports/ui/components/tabs/tabs';
import '/imports/ui/components/resultSearch/list/list'; import '/imports/ui/components/resultSearch/list/list';
import '/imports/ui/components/resultSearch/vignette/vignette';
import './resultSearch.html'; import './resultSearch.html';
......
<template name="vignetteResultSearchTpl">
<div class="panel panel-default wrapper">
<div class="panel-body">
{{#if websiteResults}}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading text-center">
<h3 class="panel-title">Résultats de site
{{#if websiteResults.time}}
<span class="small">(trouvés en {{websiteResults.time}} secondes)</span>
{{/if}}
</h3>
</div>
<div class="panel-body">
{{#each result in websiteResults.list}}
<div class="well search-result">
<div class="row">
<a href="{{result.url}}">
<div class="col-xs-6 col-sm-3 col-md-3 col-lg-2">
{{#if result.image}}
<img class="img-responsive" src="{{result.image}}" alt="{{result.title}}">
{{/if}}
</div>
<div class="col-xs-6 col-sm-9 col-md-9 col-lg-10 title">
<h3>{{result.title}}</h3>
<span class="small">{{result.url}}</span>
<p>{{truncate result.description}}</p>
</div>
</a>
</div>
</div>
{{/each}}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading text-center">
<h3 class="panel-title">Résultats de réseaux sociaux</h3>
</div>
<div class="panel-body">
{{#each result in apiResults.list}}
<li>
<a href="{{result.url}}" target="_blank">{{result.title}}</a>
</li>
{{/each}}
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading text-center">
<h3 class="panel-title">Résultats de fichiers</h3>
</div>
<div class="panel-body">
{{#each result in documentResults.list}}
<li>
{{result.attachment.title}}
</li>
{{/each}}
</div>
</div>
</div>
</div>
{{else}}
<h3 class="text-center">Aucun résultat!</h3>
{{/if}}
</div>
</div>
</template>
import { Template } from 'meteor/templating';
import truncate from 'lodash/truncate';
import './vignette.html';
Template.vignetteResultSearchTpl.helpers({
websiteResults: () => Session.get('websiteResults'),
apiResults: () => Session.get('apiResults'),
documentResults: () => Session.get('documentResults'),
truncate: term => truncate(term, {
length: 100,
}),
});
...@@ -5,14 +5,14 @@ ...@@ -5,14 +5,14 @@
<div class="autocomplete-holder"> <div class="autocomplete-holder">
{{#autoForm id="formSearch" schema=formSearchSchema resetOnSuccess=false method="post" type="method" meteormethod="searchAll" }} {{#autoForm id="formSearch" schema=formSearchSchema resetOnSuccess=false method="post" type="method" meteormethod="searchAll" }}
<div class="input-group"> <div class="input-group">
{{> afQuickField name='searchTerm' placeholder="Recherche..."}} {{> afQuickField name='searchTerm' class="form-control awesomplete" placeholder="Recherche..." }}
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</button> </button>
</span> </span>
</div> </div>
{{#if listAutoCompleteResults}} <!--{{#if listAutoCompleteResults}}
<div class="autocomplete-dropdown"> <div class="autocomplete-dropdown">
{{#each result in listAutoCompleteResults}} {{#each result in listAutoCompleteResults}}
<div class="autocomplete-row"> <div class="autocomplete-row">
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
</div> </div>
{{/each}} {{/each}}
</div> </div>
{{/if}} {{/if}}-->
{{/autoForm}} {{/autoForm}}
</div> </div>
......
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating'; import { Template } from 'meteor/templating';
import { Session } from 'meteor/session'; import { Session } from 'meteor/session';
import formSearchSchema from '/imports/api/search/formSearchSchema'; import formSearchSchema from '/imports/api/search/formSearchSchema';
import '/imports/api/search/formSearchHooks'; import '/imports/api/search/formSearchHooks';
import Awesomplete from 'awesomplete';
import 'awesomplete/awesomplete.css';
import '/imports/ui/components/resultSearch/resultSearch'; import '/imports/ui/components/resultSearch/resultSearch';
import './search.html'; import './search.html';
let awesomplete = null;
Template.searchTpl.hooks({
rendered() {
const id = $('input[name$=searchTerm]').attr('id');
awesomplete = new Awesomplete(document.querySelector(`#${id}`), {
filter(text, input) {
return text;
},
});
},
});
Template.searchTpl.helpers({ Template.searchTpl.helpers({
listAutoCompleteResults: () => Session.get('autoCompleteResults'), listAutoCompleteResults: () => Session.get('autoCompleteResults'),
formSearchSchema, formSearchSchema,
...@@ -15,18 +32,15 @@ Template.searchTpl.helpers({ ...@@ -15,18 +32,15 @@ Template.searchTpl.helpers({
Template.searchTpl.events({ Template.searchTpl.events({
'input input[name$=searchTerm]': (event) => { 'input input[name$=searchTerm]': (event) => {
event.preventDefault();
const term = event.target.value; const term = event.target.value;
if (term && term.length > 2 && term.length < 20) { if (term && term.length >= 2 && term.length < 20) {
Meteor.callPromise('autoCompletion', term) Meteor.callPromise('autoCompletion', term)
.then((results) => { .then((results) => {
Session.set('autoCompleteResults', results); awesomplete.list = results;
}); });
} }
}, },
'click .autocomplete-title': (event) => { 'click .autocomplete-title': (event) => {
event.preventDefault(); event.preventDefault();
......
...@@ -14,13 +14,14 @@ ...@@ -14,13 +14,14 @@
<hr> <hr>
<!--
<div class="row"> <div class="row">
<label>Liste des synonymes : </label> <label>Liste des synonymes : </label>
<textarea class="form-control" name="synonym" id="" cols="30" rows="10"></textarea> <textarea class="form-control" name="synonym" id="" cols="30" rows="10"></textarea>
<div class="alert alert-warning"> <div class="alert alert-warning">
<strong>Attention!</strong> Changer les synonymes nécessite une réindexation! <strong>Attention!</strong> Changer les synonymes nécessite une réindexation!
</div> </div>
</div> </div>-->
</div> </div>
</div> </div>
</template> </template>
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import $ from 'jquery';
import './demo.html'; import './demo.html';
Template.demoTpl.hooks({
rendered() {
console.log('rendu');
Meteor.callPromise('testCharge')
.then((result) => {
$('html').html(result);
});
},
});
...@@ -18,3 +18,14 @@ ...@@ -18,3 +18,14 @@
background-color: #abdde6; background-color: #abdde6;
color: #fff; color: #fff;
} }
.awesomplete {
display: block !important;
ul {
margin: 3em 0 0 !important;
}
}
.input-group-btn {
padding-top: 21px;
}
body{
background:#eee;
}
.search-result .title h3 {
margin: 0 0 15px;
font-size: 20px;
color: #333;
}
.search-result .title p {
font-size: 12px;
color: #333;
}
.well {
border: 0;
padding: 20px;
min-height: 63px;
background: #fff;
box-shadow: none;
border-radius: 3px;
position: relative;
max-height: 100000px;
border-bottom: 2px solid #ccc;
transition: max-height 0.5s ease;
-o-transition: max-height 0.5s ease;
-ms-transition: max-height 0.5s ease;
-moz-transition: max-height 0.5s ease;
-webkit-transition: max-height 0.5s ease;
}
.form-control {
height: 45px;
padding: 10px;
font-size: 16px;
box-shadow: none;
border-radius: 0;
position: relative;
}
...@@ -44,6 +44,8 @@ const checkData = { ...@@ -44,6 +44,8 @@ const checkData = {
resultText = resultText.replace(/([^A-Z'`])([A-Z])/g, '$1 $2'); resultText = resultText.replace(/([^A-Z'`])([A-Z])/g, '$1 $2');
// remplace les caractères spéciaux // remplace les caractères spéciaux
resultText = resultText.replace(/[\/<>_():\\«»"]/g, ' '); resultText = resultText.replace(/[\/<>_():\\«»"]/g, ' ');
// enleve undefined ou null
resultText = resultText.replace(/undefined|null/g, ' ');
// remplace les espaces et les retours à la ligne par un espace // remplace les espaces et les retours à la ligne par un espace
resultText = resultText.replace(/(\s{2,})/g, ' '); resultText = resultText.replace(/(\s{2,})/g, ' ');
return unfancy(resultText); return unfancy(resultText);
...@@ -62,9 +64,6 @@ const checkData = { ...@@ -62,9 +64,6 @@ const checkData = {
allowedSchemes: ['http', 'https', 'ftp', 'mailto'], allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
allowedSchemesByTag: {}, allowedSchemesByTag: {},
allowProtocolRelative: true, allowProtocolRelative: true,
transformTags: {
li: ' ',
},
}); });
}, },
......
...@@ -8,12 +8,13 @@ ...@@ -8,12 +8,13 @@
"eslint": "eslint .; exit 0" "eslint": "eslint .; exit 0"
}, },
"dependencies": { "dependencies": {
"awesomplete": "^1.1.2",
"babel-runtime": "^6.23.0", "babel-runtime": "^6.23.0",
"bcrypt": "^1.0.2", "bcrypt": "^1.0.2",
"bootstrap-sass": "^3.3.7", "bootstrap-sass": "^3.3.7",
"crawler": "^1.0.5", "crawler": "^1.0.5",
"datatables.net-bs": "^1.10.15", "datatables.net-bs": "^1.10.15",
"detergent": "^2.28.3", "detergent": "^2.30.0",
"elasticsearch": "^13.2.0", "elasticsearch": "^13.2.0",
"izitoast": "^1.1.4", "izitoast": "^1.1.4",
"jquery": "^1.11.2", "jquery": "^1.11.2",
......
...@@ -76,12 +76,21 @@ ...@@ -76,12 +76,21 @@
"resultSearch": [ "resultSearch": [
{ {
"module": "resultSearch", "module": "resultSearch",
"layout": "listResultSearch", "layout": "listResultSearchTpl",
"label": "Liste", "label": "Liste",
"state": {}, "state": {},
"activ": true, "activ": true,
"closable": false, "closable": false,