@digigov/text-search 1.0.1 → 1.1.0

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.
Files changed (69) hide show
  1. package/.eslintrc.js +3 -0
  2. package/.prettierrc.js +3 -0
  3. package/.rush/temp/operation/build/state.json +3 -0
  4. package/.rush/temp/package-deps_build.json +25 -0
  5. package/.rush/temp/package-deps_publint.json +25 -0
  6. package/.rush/temp/shrinkwrap-deps.json +158 -0
  7. package/CHANGELOG.json +79 -0
  8. package/CHANGELOG.md +8 -1
  9. package/dist/CHANGELOG.md +27 -0
  10. package/dist/README.md +70 -0
  11. package/{index.js → dist/index.js} +1 -1
  12. package/dist/package.json +21 -0
  13. package/dist/src/hook.spec.ts +289 -0
  14. package/dist/src/hook.ts +50 -0
  15. package/dist/src/index.ts +4 -0
  16. package/dist/src/search/__tests__/utils.spec.ts +73 -0
  17. package/dist/src/search/index.ts +3 -0
  18. package/dist/src/search/lang/gr/encoder.ts +27 -0
  19. package/dist/src/search/lang/gr/normalization-map.ts +100 -0
  20. package/dist/src/search/search-index.ts +103 -0
  21. package/dist/src/search/utils.ts +72 -0
  22. package/dist/src/types.ts +65 -0
  23. package/dist/test-utils/data.json +552 -0
  24. package/package.json +28 -6
  25. package/src/search/lang/gr/encoder.ts +1 -1
  26. package/text-search.build.log +13 -0
  27. package/text-search.publint.log +15 -0
  28. package/tsconfig.json +44 -0
  29. /package/{LICENSE → dist/LICENSE} +0 -0
  30. /package/{cjs → dist/cjs}/hook/index.js +0 -0
  31. /package/{cjs → dist/cjs}/hook.spec/index.js +0 -0
  32. /package/{cjs → dist/cjs}/index.js +0 -0
  33. /package/{cjs → dist/cjs}/search/__tests__/utils.spec/index.js +0 -0
  34. /package/{cjs → dist/cjs}/search/index.js +0 -0
  35. /package/{cjs → dist/cjs}/search/lang/gr/encoder/index.js +0 -0
  36. /package/{cjs → dist/cjs}/search/lang/gr/normalization-map/index.js +0 -0
  37. /package/{cjs → dist/cjs}/search/search-index/index.js +0 -0
  38. /package/{cjs → dist/cjs}/search/utils/index.js +0 -0
  39. /package/{cjs → dist/cjs}/test-utils/data.json +0 -0
  40. /package/{cjs → dist/cjs}/types/index.js +0 -0
  41. /package/{hook → dist/hook}/index.js +0 -0
  42. /package/{hook → dist/hook}/package.json +0 -0
  43. /package/{hook.d.ts → dist/hook.d.ts} +0 -0
  44. /package/{hook.spec → dist/hook.spec}/index.js +0 -0
  45. /package/{hook.spec → dist/hook.spec}/package.json +0 -0
  46. /package/{hook.spec.d.ts → dist/hook.spec.d.ts} +0 -0
  47. /package/{index.d.ts → dist/index.d.ts} +0 -0
  48. /package/{search → dist/search}/__tests__/utils.spec/index.js +0 -0
  49. /package/{search → dist/search}/__tests__/utils.spec/package.json +0 -0
  50. /package/{search → dist/search}/__tests__/utils.spec.d.ts +0 -0
  51. /package/{search → dist/search}/index.d.ts +0 -0
  52. /package/{search → dist/search}/index.js +0 -0
  53. /package/{search → dist/search}/lang/gr/encoder/index.js +0 -0
  54. /package/{search → dist/search}/lang/gr/encoder/package.json +0 -0
  55. /package/{search → dist/search}/lang/gr/encoder.d.ts +0 -0
  56. /package/{search → dist/search}/lang/gr/normalization-map/index.js +0 -0
  57. /package/{search → dist/search}/lang/gr/normalization-map/package.json +0 -0
  58. /package/{search → dist/search}/lang/gr/normalization-map.d.ts +0 -0
  59. /package/{search → dist/search}/package.json +0 -0
  60. /package/{search → dist/search}/search-index/index.js +0 -0
  61. /package/{search → dist/search}/search-index/package.json +0 -0
  62. /package/{search → dist/search}/search-index.d.ts +0 -0
  63. /package/{search → dist/search}/utils/index.js +0 -0
  64. /package/{search → dist/search}/utils/package.json +0 -0
  65. /package/{search → dist/search}/utils.d.ts +0 -0
  66. /package/{test-utils → dist/src/test-utils}/data.json +0 -0
  67. /package/{types → dist/types}/index.js +0 -0
  68. /package/{types → dist/types}/package.json +0 -0
  69. /package/{types.d.ts → dist/types.d.ts} +0 -0
@@ -0,0 +1,289 @@
1
+ import { act, renderHook } from '@testing-library/react-hooks';
2
+ import useSearch from './hook';
3
+ import documents from './test-utils/data.json';
4
+
5
+ describe('loading', () => {
6
+ it('should start when searching', async () => {
7
+ const { result } = renderHook(() => useSearch(documents, 'Anastasia'));
8
+ expect(result.current.loading).toBe(false);
9
+ act(() => result.current.search());
10
+ expect(result.current.loading).toBe(true);
11
+ });
12
+ it('should stop after search is completed', async () => {
13
+ const { result, waitForNextUpdate } = renderHook(() =>
14
+ useSearch(documents, 'Anastasia')
15
+ );
16
+ expect(result.current.loading).toBe(false);
17
+ act(() => result.current.search());
18
+ expect(result.current.loading).toBe(true);
19
+ await waitForNextUpdate();
20
+ expect(result.current.loading).toBe(false);
21
+ });
22
+ });
23
+
24
+ describe('reset', () => {
25
+ it('should change the returned result back to the initial data', async () => {
26
+ const { result, waitForNextUpdate } = renderHook(() =>
27
+ useSearch(documents, 'Anastasia')
28
+ );
29
+ expect(result.current.loading).toBe(false);
30
+ act(() => result.current.search());
31
+ expect(result.current.loading).toBe(true);
32
+ expect(result.current.data).toEqual(documents);
33
+ await waitForNextUpdate();
34
+ expect(result.current.loading).toBe(false);
35
+ expect(result.current.data.length).toBe(1);
36
+
37
+ act(() => result.current.reset());
38
+ expect(result.current.data).toEqual(documents);
39
+ expect(result.current.loading).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe('search', () => {
44
+ it('should return the initial data when no query is passed', async () => {
45
+ const { result } = renderHook(() => useSearch(documents));
46
+ expect(result.current.loading).toBe(false);
47
+ act(() => result.current.search());
48
+ expect(result.current.loading).toBe(false);
49
+ expect(result.current.data).toEqual(documents);
50
+ });
51
+ it('should return an empty array when there are no relevant items', async () => {
52
+ const { result, waitForNextUpdate } = renderHook(() =>
53
+ useSearch(documents, 'fajsgsdgsdgsd')
54
+ );
55
+ expect(result.current.loading).toBe(false);
56
+ act(() => result.current.search());
57
+ expect(result.current.loading).toBe(true);
58
+ expect(result.current.data).toEqual(documents);
59
+ await waitForNextUpdate();
60
+ expect(result.current.loading).toBe(false);
61
+ expect(result.current.data).toEqual([]);
62
+ });
63
+ it('should return an empty array when relevant field is not indexed', async () => {
64
+ const { result, waitForNextUpdate } = renderHook(() =>
65
+ useSearch(documents, 'Anastasia', {
66
+ indexing: {
67
+ fields: ['lastName'], // missing 'firstName'
68
+ },
69
+ })
70
+ );
71
+ expect(result.current.loading).toBe(false);
72
+ act(() => result.current.search());
73
+ expect(result.current.loading).toBe(true);
74
+ expect(result.current.data).toEqual(documents);
75
+ await waitForNextUpdate();
76
+ expect(result.current.loading).toBe(false);
77
+ expect(result.current.data).toEqual([]);
78
+ });
79
+ it('should return a single result when there is only one relevant item', async () => {
80
+ const { result, waitForNextUpdate } = renderHook(() =>
81
+ useSearch(documents, 'Anastasia')
82
+ );
83
+ expect(result.current.loading).toBe(false);
84
+ act(() => result.current.search());
85
+ expect(result.current.loading).toBe(true);
86
+ expect(result.current.data).toEqual(documents);
87
+ await waitForNextUpdate();
88
+ expect(result.current.loading).toBe(false);
89
+ expect(result.current.data.length).toBe(1);
90
+
91
+ const firstResult = result.current.data[0] as NonNullable<
92
+ typeof result.current.data[0]
93
+ >;
94
+ expect(firstResult).toBeDefined();
95
+ expect(Object.values(firstResult).values()).toContain('Anastasia');
96
+ });
97
+ it('should return multiple results when there are multiple relevant items', async () => {
98
+ const { result, waitForNextUpdate } = renderHook(() =>
99
+ useSearch(documents, 'Kari')
100
+ );
101
+ expect(result.current.loading).toBe(false);
102
+ act(() => result.current.search());
103
+ expect(result.current.loading).toBe(true);
104
+ expect(result.current.data).toEqual(documents);
105
+ await waitForNextUpdate();
106
+ expect(result.current.loading).toBe(false);
107
+ expect(result.current.data.length).toBeGreaterThanOrEqual(2);
108
+
109
+ result.current.data.forEach((item) => {
110
+ expect(item).toBeDefined();
111
+ expect(Object.values(item).values()).toContain('Kari');
112
+ });
113
+ });
114
+ it('should work with a partial query', async () => {
115
+ const { result, waitForNextUpdate } = renderHook(() =>
116
+ useSearch(documents, 'anast')
117
+ );
118
+ expect(result.current.loading).toBe(false);
119
+ act(() => result.current.search());
120
+ expect(result.current.loading).toBe(true);
121
+ expect(result.current.data).toEqual(documents);
122
+ await waitForNextUpdate();
123
+ expect(result.current.loading).toBe(false);
124
+ expect(result.current.data.length).toBe(1);
125
+
126
+ const firstResult = result.current.data[0] as NonNullable<
127
+ typeof result.current.data[0]
128
+ >;
129
+ expect(firstResult).toBeDefined();
130
+ expect(Object.values(firstResult).values()).toContain('Anastasia');
131
+ });
132
+ it('should work with nested field if not explicitly stated', async () => {
133
+ const { result, waitForNextUpdate } = renderHook(() =>
134
+ useSearch(documents, 'Titanic')
135
+ );
136
+ expect(result.current.loading).toBe(false);
137
+ act(() => result.current.search());
138
+ expect(result.current.loading).toBe(true);
139
+ expect(result.current.data).toEqual(documents);
140
+ await waitForNextUpdate();
141
+ expect(result.current.loading).toBe(false);
142
+ expect(result.current.data.length).toBe(1);
143
+
144
+ const firstResult = result.current.data[0] as NonNullable<
145
+ typeof result.current.data[0]
146
+ >;
147
+ expect(firstResult).toBeDefined();
148
+ expect(Object.values(firstResult.address).values()).toContain('Titanic');
149
+ });
150
+ it('should work with nested field if explicitly stated', async () => {
151
+ const { result, waitForNextUpdate } = renderHook(() =>
152
+ useSearch(documents, 'Titanic', {
153
+ indexing: {
154
+ fields: ['address.city'],
155
+ },
156
+ })
157
+ );
158
+ expect(result.current.loading).toBe(false);
159
+ act(() => result.current.search());
160
+ expect(result.current.loading).toBe(true);
161
+ expect(result.current.data).toEqual(documents);
162
+ await waitForNextUpdate();
163
+ expect(result.current.loading).toBe(false);
164
+ expect(result.current.data.length).toBe(1);
165
+
166
+ const firstResult = result.current.data[0] as NonNullable<
167
+ typeof result.current.data[0]
168
+ >;
169
+ expect(firstResult).toBeDefined();
170
+ expect(Object.values(firstResult.address).values()).toContain('Titanic');
171
+ });
172
+ it('should work with nested ID', async () => {
173
+ const { result, waitForNextUpdate } = renderHook(() =>
174
+ useSearch(documents, 'Titanic', {
175
+ indexing: {
176
+ idKey: 'address.street',
177
+ },
178
+ })
179
+ );
180
+ expect(result.current.loading).toBe(false);
181
+ act(() => result.current.search());
182
+ expect(result.current.loading).toBe(true);
183
+ expect(result.current.data).toEqual(documents);
184
+ await waitForNextUpdate();
185
+ expect(result.current.loading).toBe(false);
186
+ expect(result.current.data.length).toBe(1);
187
+
188
+ const firstResult = result.current.data[0] as NonNullable<
189
+ typeof result.current.data[0]
190
+ >;
191
+ expect(firstResult).toBeDefined();
192
+ expect(Object.values(firstResult.address).values()).toContain('Titanic');
193
+ });
194
+ it('should work with indexed Greek characters and Latin query', async () => {
195
+ const { result, waitForNextUpdate } = renderHook(() =>
196
+ useSearch(documents, 'Markos')
197
+ );
198
+ expect(result.current.loading).toBe(false);
199
+ act(() => result.current.search());
200
+ expect(result.current.loading).toBe(true);
201
+ expect(result.current.data).toEqual(documents);
202
+ await waitForNextUpdate();
203
+ expect(result.current.loading).toBe(false);
204
+ expect(result.current.data.length).toBe(1);
205
+
206
+ const firstResult = result.current.data[0] as NonNullable<
207
+ typeof result.current.data[0]
208
+ >;
209
+ expect(firstResult).toBeDefined();
210
+ expect(Object.values(firstResult).values()).toContain('Μάρκος');
211
+ });
212
+ it('should work with indexed Greek characters and Greek query', async () => {
213
+ const { result, waitForNextUpdate } = renderHook(() =>
214
+ useSearch(documents, 'Μάρκος')
215
+ );
216
+ expect(result.current.loading).toBe(false);
217
+ act(() => result.current.search());
218
+ expect(result.current.loading).toBe(true);
219
+ expect(result.current.data).toEqual(documents);
220
+ await waitForNextUpdate();
221
+ expect(result.current.loading).toBe(false);
222
+ expect(result.current.data.length).toBe(1);
223
+
224
+ const firstResult = result.current.data[0] as NonNullable<
225
+ typeof result.current.data[0]
226
+ >;
227
+ expect(firstResult).toBeDefined();
228
+ expect(Object.values(firstResult).values()).toContain('Μάρκος');
229
+ });
230
+ it('should react to changes in the documents list', async () => {
231
+ const emptyDocuments: typeof documents = [];
232
+ const { result, rerender, waitForNextUpdate } = renderHook(
233
+ ({ documents }) => useSearch(documents, 'Anastasia'),
234
+ {
235
+ initialProps: { documents: emptyDocuments },
236
+ }
237
+ );
238
+ expect(result.current.loading).toBe(false);
239
+ act(() => result.current.search());
240
+ expect(result.current.loading).toBe(true);
241
+ await waitForNextUpdate();
242
+ expect(result.current.loading).toBe(false);
243
+ expect(result.current.data).toEqual([]);
244
+
245
+ rerender({ documents });
246
+
247
+ expect(result.current.loading).toBe(false);
248
+ expect(result.current.data).toEqual([]);
249
+ act(() => result.current.search());
250
+ expect(result.current.loading).toBe(true);
251
+ expect(result.current.data).toEqual([]);
252
+ await waitForNextUpdate();
253
+ expect(result.current.loading).toBe(false);
254
+ expect(result.current.data.length).toBe(1);
255
+
256
+ const firstResult = result.current.data[0] as NonNullable<
257
+ typeof result.current.data[0]
258
+ >;
259
+ expect(firstResult).toBeDefined();
260
+ expect(Object.values(firstResult).values()).toContain('Anastasia');
261
+ });
262
+ it('should react to changes in the search query', async () => {
263
+ const { result, rerender, waitForNextUpdate } = renderHook(
264
+ ({ query }) => useSearch(documents, query),
265
+ {
266
+ initialProps: { query: '' },
267
+ }
268
+ );
269
+ expect(result.current.loading).toBe(false);
270
+ expect(result.current.data).toEqual(documents);
271
+
272
+ rerender({ query: 'Anastasia' });
273
+
274
+ expect(result.current.loading).toBe(false);
275
+ expect(result.current.data).toEqual(documents);
276
+ act(() => result.current.search());
277
+ expect(result.current.loading).toBe(true);
278
+ expect(result.current.data).toEqual(documents);
279
+ await waitForNextUpdate();
280
+ expect(result.current.loading).toBe(false);
281
+ expect(result.current.data.length).toBe(1);
282
+
283
+ const firstResult = result.current.data[0] as NonNullable<
284
+ typeof result.current.data[0]
285
+ >;
286
+ expect(firstResult).toBeDefined();
287
+ expect(Object.values(firstResult).values()).toContain('Anastasia');
288
+ });
289
+ });
@@ -0,0 +1,50 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import SearchIndex from './search';
3
+ import { UseSearchOptions } from './types';
4
+
5
+ /**
6
+ * Hook for searching through a list of documents
7
+ *
8
+ * Returns a list of documents that match the search query.
9
+ * If no query is provided, it returns the original list of documents.
10
+ *
11
+ * @param documents - The list of documents to search through
12
+ * @param options - Options for configuring the search index
13
+ * @param query - The search query
14
+ *
15
+ * @typeParam T - The type of the data in the documents list
16
+ */
17
+ export default function useSearch<T extends Record<string, any>>(
18
+ documents: T[],
19
+ query?: string,
20
+ options?: UseSearchOptions<T>
21
+ ) {
22
+ const [loading, setLoading] = useState(false);
23
+ const [data, setData] = useState(documents);
24
+
25
+ const indexing = options?.indexing;
26
+
27
+ const index = useMemo(() => new SearchIndex(documents, indexing), [
28
+ documents,
29
+ indexing,
30
+ ]);
31
+
32
+ const search = useCallback(() => {
33
+ if (query) {
34
+ setLoading(true);
35
+ index
36
+ .searchAsync(documents, query)
37
+ .then((data) => setData(data))
38
+ .finally(() => setLoading(false));
39
+ } else {
40
+ setData(documents);
41
+ }
42
+ }, [query, index, documents]);
43
+
44
+ const reset = useCallback(() => {
45
+ setData(documents);
46
+ setLoading(false);
47
+ }, [documents]);
48
+
49
+ return { data, loading, search, reset };
50
+ }
@@ -0,0 +1,4 @@
1
+ import useSearch from './hook';
2
+
3
+ export default useSearch;
4
+ export * from './search';
@@ -0,0 +1,73 @@
1
+ import type { SimpleDocumentSearchResultSetUnit as ResultType } from 'flexsearch';
2
+ import { findItemsByIds, getAllItemKeys, getResultIds } from '../utils';
3
+
4
+ describe('utils', () => {
5
+ describe('getResultIds', () => {
6
+ it('should return the ids of the results without duplicates', () => {
7
+ const result: ResultType[] = [
8
+ { field: 'title', result: [1, 2] },
9
+ { field: 'description', result: [2, 3] },
10
+ { field: 'tag', result: [1, 2, 3, 4] },
11
+ ];
12
+ expect(getResultIds(result)).toEqual([1, 2, 3, 4]);
13
+ });
14
+ it('should return an empty array if there are no results', () => {
15
+ const result: ResultType[] = [];
16
+ expect(getResultIds(result)).toEqual([]);
17
+ });
18
+ });
19
+
20
+ describe('findItemsByIds', () => {
21
+ it('should return the items that match the given ids', () => {
22
+ const items = [
23
+ { id: 1, name: 'Anastasia' },
24
+ { id: 2, name: 'Bob' },
25
+ { id: 3, name: 'Cindy' },
26
+ ];
27
+ const ids = [1, 3];
28
+ expect(findItemsByIds(items, ids, 'id')).toEqual([
29
+ { id: 1, name: 'Anastasia' },
30
+ { id: 3, name: 'Cindy' },
31
+ ]);
32
+ });
33
+ it('should return an empty array if there are no items', () => {
34
+ const items: Record<string, any>[] = [];
35
+ const ids = [1, 3];
36
+ expect(findItemsByIds(items, ids, 'id')).toEqual([]);
37
+ });
38
+ it('should return an empty array if there are no ids', () => {
39
+ const items = [
40
+ { id: 1, name: 'Anastasia' },
41
+ { id: 2, name: 'Bob' },
42
+ { id: 3, name: 'Cindy' },
43
+ ];
44
+ const ids: number[] = [];
45
+ expect(findItemsByIds(items, ids, 'id')).toEqual([]);
46
+ });
47
+ });
48
+
49
+ describe('getAllItemKeys', () => {
50
+ it('should return all the keys for the given item', () => {
51
+ const item = {
52
+ id: 1,
53
+ name: 'Anastasia',
54
+ address: {
55
+ street: '123 Main St',
56
+ city: 'New York',
57
+ state: 'NY',
58
+ },
59
+ };
60
+ expect(getAllItemKeys(item)).toEqual([
61
+ 'id',
62
+ 'name',
63
+ 'address.street',
64
+ 'address.city',
65
+ 'address.state',
66
+ ]);
67
+ });
68
+ it('should return an empty array if item is an empty object', () => {
69
+ const item: Record<string, any> = {};
70
+ expect(getAllItemKeys(item)).toEqual([]);
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,3 @@
1
+ import { SearchIndex } from './search-index';
2
+
3
+ export default SearchIndex;
@@ -0,0 +1,27 @@
1
+ import { greekToGreeklishMap } from './normalization-map';
2
+
3
+ const whitespaceRegex = /[\p{Z}\p{S}\p{P}\p{C}]+/u;
4
+ const diacriticsRegex = /[\u0300-\u036f]/g;
5
+ const greekCharsRegex = new RegExp(
6
+ Object.keys(greekToGreeklishMap).join('|'),
7
+ 'igu'
8
+ );
9
+
10
+ /**
11
+ * Encode a string containing greek characters to greeklish
12
+ *
13
+ * This function is used to encode and tokenize a string containing greek characters.
14
+ * It replaces greek characters with their latin counterparts, while also removing
15
+ * any diacritics.
16
+ *
17
+ * @param str - The string to encode
18
+ * @returns An array of encoded words
19
+ */
20
+ export function encodeGreek(str: string) {
21
+ return ('' + str)
22
+ .toLowerCase()
23
+ .normalize('NFD')
24
+ .replace(diacriticsRegex, '')
25
+ .replace(greekCharsRegex, (match) => (greekToGreeklishMap as any)[match])
26
+ .split(whitespaceRegex);
27
+ }
@@ -0,0 +1,100 @@
1
+ export const greekToGreeklishMap = {
2
+ ΓΧ: 'GX',
3
+ γχ: 'gx',
4
+ ΤΘ: 'T8',
5
+ τθ: 't8',
6
+ θη: '8h',
7
+ Θη: '8h',
8
+ ΘΗ: '8H',
9
+ αυ: 'au',
10
+ Αυ: 'Au',
11
+ ΑΥ: 'AY',
12
+ ευ: 'eu',
13
+ εύ: 'eu',
14
+ εϋ: 'ey',
15
+ εΰ: 'ey',
16
+ Ευ: 'Eu',
17
+ Εύ: 'Eu',
18
+ Εϋ: 'Ey',
19
+ Εΰ: 'Ey',
20
+ ΕΥ: 'EY',
21
+ ου: 'ou',
22
+ ού: 'ou',
23
+ οϋ: 'oy',
24
+ οΰ: 'oy',
25
+ Ου: 'Ou',
26
+ Ού: 'Ou',
27
+ Οϋ: 'Oy',
28
+ Οΰ: 'Oy',
29
+ ΟΥ: 'OY',
30
+ Α: 'A',
31
+ α: 'a',
32
+ ά: 'a',
33
+ Ά: 'A',
34
+ Β: 'B',
35
+ β: 'b',
36
+ Γ: 'G',
37
+ γ: 'g',
38
+ Δ: 'D',
39
+ δ: 'd',
40
+ Ε: 'E',
41
+ ε: 'e',
42
+ έ: 'e',
43
+ Έ: 'E',
44
+ Ζ: 'Z',
45
+ ζ: 'z',
46
+ Η: 'H',
47
+ η: 'h',
48
+ ή: 'h',
49
+ Ή: 'H',
50
+ Θ: 'TH',
51
+ θ: 'th',
52
+ Ι: 'I',
53
+ Ϊ: 'I',
54
+ ι: 'i',
55
+ ί: 'i',
56
+ ΐ: 'i',
57
+ ϊ: 'i',
58
+ Ί: 'I',
59
+ Κ: 'K',
60
+ κ: 'k',
61
+ Λ: 'L',
62
+ λ: 'l',
63
+ Μ: 'M',
64
+ μ: 'm',
65
+ Ν: 'N',
66
+ ν: 'n',
67
+ Ξ: 'KS',
68
+ ξ: 'ks',
69
+ Ο: 'O',
70
+ ο: 'o',
71
+ Ό: 'O',
72
+ ό: 'o',
73
+ Π: 'P',
74
+ π: 'p',
75
+ Ρ: 'R',
76
+ ρ: 'r',
77
+ Σ: 'S',
78
+ σ: 's',
79
+ Τ: 'T',
80
+ τ: 't',
81
+ Υ: 'Y',
82
+ Ύ: 'Y',
83
+ Ϋ: 'Y',
84
+ ΰ: 'y',
85
+ ύ: 'y',
86
+ ϋ: 'y',
87
+ υ: 'y',
88
+ Φ: 'F',
89
+ φ: 'f',
90
+ Χ: 'X',
91
+ χ: 'x',
92
+ Ψ: 'Ps',
93
+ ψ: 'ps',
94
+ Ω: 'w',
95
+ ω: 'w',
96
+ Ώ: 'w',
97
+ ώ: 'w',
98
+ ς: 's',
99
+ ';': '?',
100
+ };
@@ -0,0 +1,103 @@
1
+ import { Document } from 'flexsearch';
2
+ import type { SearchIndexOptions } from '../types';
3
+ import { encodeGreek } from './lang/gr/encoder';
4
+ import { findItemsByIds, getAllItemKeys, getResultIds } from './utils';
5
+
6
+ /**
7
+ * Wrapper class around third party index and search library
8
+ *
9
+ * @typeParam T - The type of the data that will be indexed
10
+ */
11
+ export class SearchIndex<T extends Record<string, any>> {
12
+ /**
13
+ * Instance of the third party library's index
14
+ */
15
+ private index: Document<T>;
16
+ private idKey: string;
17
+
18
+ constructor(items: T[], options?: SearchIndexOptions<T>) {
19
+ this.idKey = 'id';
20
+ if (options && options.idKey) {
21
+ this.idKey = options.idKey.replace('.', ':');
22
+ } else if (!(items[0] && 'id' in items[0])) {
23
+ items = items.map((item, index) => ({ ...item, id: index }));
24
+ }
25
+
26
+ let fields: string[] = [];
27
+ if (options && options.fields) {
28
+ fields = options.fields;
29
+ } else if (items[0]) {
30
+ fields = getAllItemKeys(items[0]);
31
+ }
32
+ fields = fields
33
+ .filter((field) => field !== this.idKey)
34
+ .map((field) => field.replace('.', ':'));
35
+
36
+ this.index = new Document<T>({
37
+ document: {
38
+ id: this.idKey,
39
+ index: fields,
40
+ },
41
+ tokenize: 'forward',
42
+ worker: options?.enableWorker,
43
+ encode: encodeGreek,
44
+ });
45
+
46
+ this.addAll(items);
47
+ }
48
+
49
+ /**
50
+ * Add an item to the index
51
+ *
52
+ * @param item - The item to add to the index
53
+ */
54
+ public add(item: T) {
55
+ this.index.add(item);
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Add multiple items to the index
61
+ *
62
+ * @param items - The list of items to add to the index
63
+ */
64
+ public addAll(items: T[]) {
65
+ items.forEach((item) => this.index.add(item));
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * Search the index for the given term
71
+ *
72
+ * @param documents - The list of documents to search
73
+ * @param searchTerm - The term to search for
74
+ * @returns The list of items that match the search term
75
+ */
76
+ public search(documents: T[], searchTerm: string) {
77
+ const res = this.index.search(searchTerm);
78
+ const ids = getResultIds(res);
79
+ return findItemsByIds(
80
+ documents,
81
+ ids,
82
+ this.idKey as Extract<keyof T, string>
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Asynchronously search the index for the given term
88
+ *
89
+ * @param documents - The list of documents to search
90
+ * @param searchTerm - The term to search for
91
+ * @returns The list of items that match the search term
92
+ */
93
+ public async searchAsync(documents: T[], searchTerm: string) {
94
+ return this.index
95
+ .searchAsync(searchTerm)
96
+ .then((res) => {
97
+ return getResultIds(res);
98
+ })
99
+ .then((res) =>
100
+ findItemsByIds(documents, res, this.idKey as Extract<keyof T, string>)
101
+ );
102
+ }
103
+ }