@tekkare/romulus 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Les options d'un filtre a referentiel long, cherchees dans l'index.
|
|
3
|
+
*
|
|
4
|
+
* Un filtre agrege un champ (les marques, les molecules) et cherche sur
|
|
5
|
+
* d'autres. La regle qui compte est la : sur un index ou une ligne porte a la
|
|
6
|
+
* fois la marque, la molecule, la classe et le laboratoire, chercher dans un
|
|
7
|
+
* filtre doit interroger AUSSI les champs freres, sans changer ce que le
|
|
8
|
+
* filtre remonte. « keytruda » tape dans le filtre molecule trouve la ligne
|
|
9
|
+
* par sa marque et l'agregation en sort PEMBROLIZUMAB ; « pembrolizumab »
|
|
10
|
+
* tape dans le filtre marque en sort Keytruda. Sans cela l'utilisateur doit
|
|
11
|
+
* connaitre le nom qu'il cherche dans le vocabulaire exact du filtre, ce qui
|
|
12
|
+
* est justement ce qu'il vient demander.
|
|
13
|
+
*
|
|
14
|
+
* Le module ne parle pas au moteur : il ecrit le corps de la requete et relit
|
|
15
|
+
* les seaux. Le transport (proxy, cache, index) reste a l'application.
|
|
16
|
+
*/
|
|
17
|
+
export interface RmFilterOption {
|
|
18
|
+
label: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
export interface RmFilterOptionsSpec {
|
|
22
|
+
/**
|
|
23
|
+
* Ce que le filtre agrege. Un champ : la valeur est son propre libelle
|
|
24
|
+
* (`brand.keyword`). Deux champs `[libelle, valeur]` : le premier s'affiche,
|
|
25
|
+
* le second est ce que l'analyse filtre (un libelle ATC et son code).
|
|
26
|
+
*/
|
|
27
|
+
field: string | [string, string];
|
|
28
|
+
/**
|
|
29
|
+
* Champs interroges par la saisie, dans l'ordre : le champ propre au filtre
|
|
30
|
+
* d'abord, ses synonymes ensuite. Champs analyses (`brand`), pas leur
|
|
31
|
+
* `.keyword` : un `.keyword` ne repond pas a une recherche partielle.
|
|
32
|
+
*/
|
|
33
|
+
search: string[];
|
|
34
|
+
/** La saisie, telle qu'elle est tapee. Vide, tout le referentiel remonte. */
|
|
35
|
+
input: string;
|
|
36
|
+
/**
|
|
37
|
+
* Plafond de la liste. Au-dela, c'est la recherche qui trie : une liste
|
|
38
|
+
* coupee a cent laisse croire que ce qui suit n'existe pas.
|
|
39
|
+
*/
|
|
40
|
+
size?: number;
|
|
41
|
+
}
|
|
42
|
+
/** Le corps de la requete d'agregation, a poster tel quel sur l'index. */
|
|
43
|
+
export declare function rmFilterOptionsQuery(spec: RmFilterOptionsSpec): {
|
|
44
|
+
size: number;
|
|
45
|
+
query: {
|
|
46
|
+
bool: {
|
|
47
|
+
must: {
|
|
48
|
+
query_string: {
|
|
49
|
+
fields: string[];
|
|
50
|
+
query: string;
|
|
51
|
+
analyze_wildcard: boolean;
|
|
52
|
+
default_operator: string;
|
|
53
|
+
};
|
|
54
|
+
}[];
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
aggs: {
|
|
58
|
+
count: {
|
|
59
|
+
multi_terms: {
|
|
60
|
+
terms: {
|
|
61
|
+
field: string;
|
|
62
|
+
}[];
|
|
63
|
+
size: number;
|
|
64
|
+
order: {
|
|
65
|
+
_key: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
terms?: undefined;
|
|
69
|
+
} | {
|
|
70
|
+
terms: {
|
|
71
|
+
field: string;
|
|
72
|
+
size: number;
|
|
73
|
+
order: {
|
|
74
|
+
_key: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
multi_terms?: undefined;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/** Les options, relues dans la reponse du moteur. */
|
|
82
|
+
export declare function rmFilterOptions(response: any): RmFilterOption[];
|
|
83
|
+
/** Un champ interroge par la saisie d'un filtre. */
|
|
84
|
+
export interface RmFilterSearchField {
|
|
85
|
+
/** Champ interroge. La recherche porte sur son `.keyword`. */
|
|
86
|
+
field: string;
|
|
87
|
+
/**
|
|
88
|
+
* Chemins nested a traverser pour l'atteindre, du plus large au plus
|
|
89
|
+
* etroit (`['brand_history']`, ou `['a', 'a.b']` pour deux niveaux).
|
|
90
|
+
* Absent, le champ est a la racine du document.
|
|
91
|
+
*/
|
|
92
|
+
nested?: string[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* La clause qui restreint les documents a la saisie, sur plusieurs champs a la
|
|
96
|
+
* fois. Meme regle que `rmFilterOptionsQuery`, pour un index ou le champ frere
|
|
97
|
+
* ne vit pas au meme niveau que le champ agrege : le filtre marque d'un index
|
|
98
|
+
* d'avis agrege `brand_group` a la racine et cherche la molecule dans
|
|
99
|
+
* `brand_history`, si bien que « pembro » sort KEYTRUDA. Un champ nested
|
|
100
|
+
* interroge sans son enveloppe `nested` ne repond jamais : c'est un filtre qui
|
|
101
|
+
* parait vide plutot qu'un filtre en erreur, d'ou cette enveloppe explicite.
|
|
102
|
+
*
|
|
103
|
+
* Trois facons de reconnaitre la saisie par champ : le fragment n'importe ou
|
|
104
|
+
* (`regexp`), la valeur entiere, et le debut. La derniere remonte en tete des
|
|
105
|
+
* listes ordonnees par pertinence.
|
|
106
|
+
*/
|
|
107
|
+
export declare function rmFilterSearchClause(fields: RmFilterSearchField[], input: string): {
|
|
108
|
+
bool: {
|
|
109
|
+
should: any[];
|
|
110
|
+
};
|
|
111
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const DEFAULT_SIZE = 5e3;
|
|
2
|
+
function wildcard(input) {
|
|
3
|
+
return ("*" + input + "*").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
4
|
+
}
|
|
5
|
+
export function rmFilterOptionsQuery(spec) {
|
|
6
|
+
const size = spec.size ?? DEFAULT_SIZE;
|
|
7
|
+
const count = Array.isArray(spec.field) ? {
|
|
8
|
+
multi_terms: {
|
|
9
|
+
terms: spec.field.map((field) => ({ field })),
|
|
10
|
+
size,
|
|
11
|
+
order: { _key: "asc" }
|
|
12
|
+
}
|
|
13
|
+
} : { terms: { field: spec.field, size, order: { _key: "asc" } } };
|
|
14
|
+
return {
|
|
15
|
+
size: 0,
|
|
16
|
+
query: {
|
|
17
|
+
bool: {
|
|
18
|
+
must: [
|
|
19
|
+
{
|
|
20
|
+
query_string: {
|
|
21
|
+
// Plusieurs champs valent un OU : la ligne repond des qu'un seul
|
|
22
|
+
// d'entre eux porte la saisie. C'est ce qui rend les synonymes.
|
|
23
|
+
fields: spec.search,
|
|
24
|
+
query: wildcard(spec.input),
|
|
25
|
+
analyze_wildcard: true,
|
|
26
|
+
default_operator: "AND"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
aggs: { count }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function rmFilterOptions(response) {
|
|
36
|
+
const buckets = response?.aggregations?.count?.buckets ?? [];
|
|
37
|
+
return buckets.map(
|
|
38
|
+
(bucket) => Array.isArray(bucket.key) ? { label: String(bucket.key[0]), value: String(bucket.key[1]) } : { label: String(bucket.key), value: String(bucket.key) }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
export function rmFilterSearchClause(fields, input) {
|
|
42
|
+
return {
|
|
43
|
+
bool: {
|
|
44
|
+
should: fields.map(({ field, nested }) => {
|
|
45
|
+
const clause = {
|
|
46
|
+
bool: {
|
|
47
|
+
should: [
|
|
48
|
+
{
|
|
49
|
+
regexp: {
|
|
50
|
+
[`${field}.keyword`]: {
|
|
51
|
+
value: `.*${input}.*`,
|
|
52
|
+
flags: "ALL",
|
|
53
|
+
case_insensitive: true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{ match_phrase: { [`${field}.keyword`]: input } },
|
|
58
|
+
{ prefix: { [`${field}.keyword`]: input } }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return (nested ?? []).reduceRight(
|
|
63
|
+
(query, path) => ({ nested: { path, query } }),
|
|
64
|
+
clause
|
|
65
|
+
);
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|