Skip to content

Commit 5ae1890

Browse files
committed
edit: 40/40 passing tests, multikey is beta
the big drawback is that there are lots of ways for user to input incorrect objects. Typescript is super helpful, but even still its a lot of strings and complexity. easy to enter wrong data etc. its part of the nature of offering cross-collection, cross-analyzer, cross-key boolean text search. but i believe the middle ground is accessible to most.
1 parent 4540ca7 commit 5ae1890

File tree

9 files changed

+209
-318
lines changed

9 files changed

+209
-318
lines changed

src/index.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ export function buildAQL(
1818
limit: any = { start: 0, count: 20 },
1919
): any {
2020
validateQuery(query)
21-
/* unify query.key */
22-
query.key = query.key ? query.key : ['text']
23-
query.key = typeof query.key == 'string' ? [query.key] : query.key
21+
collectKeys(query)
2422

2523
const SEARCH = buildSearch(query)
2624
const FILTER = query.filters && buildFilters(query.filters)
@@ -40,3 +38,18 @@ function validateQuery(query: query) {
4038
if (!query.collections.length)
4139
throw new Error('query.collections must have at least one name')
4240
}
41+
42+
function collectKeys(query: query) {
43+
/* unify query.key */
44+
let _keys: string[]
45+
if (typeof query.key == 'string') {
46+
_keys = [query.key]
47+
} else if (!query.key) {
48+
_keys = ['text']
49+
} else _keys = query.key
50+
51+
query.collections = query.collections.map((c) => {
52+
if (!c.keys) c.keys = _keys
53+
return c
54+
})
55+
}

src/lib/structs.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export interface query {
88
* */
99
view: string
1010
/**
11-
* the names of the collections indexed by @param view to query
11+
* the names and analyzers of the collections indexed by @param
12+
* view to query
1213
* */
1314
collections: collection[]
1415
/**
@@ -31,21 +32,24 @@ export interface query {
3132
*
3233
* A collection can be referenced by several analyzers and each must have its
3334
* own entry in `query.collections` in order to be included in the search.
35+
* Also, if query.key
3436
*
3537
* Alternatively, a document can be stored in several collections.
3638
*
3739
* In either case all desired collection/analyzer combinations must be
3840
* specified.
3941
* */
4042
export interface collection {
41-
/**
42-
* the name of the collection
43-
* */
43+
/** the name of the collection */
4444
name: string
45-
/**
46-
* the name of the text analyzer
47-
* */
45+
/** the name of the text analyzer */
4846
analyzer: string
47+
/*
48+
* a list of key names that are indexed by the analyzer that you wish to
49+
* query against. If none are provided, those provided in @param query.key
50+
* will be used
51+
* */
52+
keys?: string[]
4953
}
5054

5155
/**

src/search.ts

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ export function buildSearch(query: query): any {
77
query.terms =
88
typeof query.terms == 'string' ? parseQuery(query.terms) : query.terms
99

10-
query.key = typeof query.key == 'string' ? [query.key] : query.key
11-
1210
/* build boolean pieces */
13-
let ANDS = buildOps(query.collections, query.terms, '+', query.key)
14-
let ORS = buildOps(query.collections, query.terms, '?', query.key)
15-
let NOTS = buildOps(query.collections, query.terms, '-', query.key)
11+
let ANDS = buildOps(query.collections, query.terms, '+')
12+
let ORS = buildOps(query.collections, query.terms, '?')
13+
let NOTS = buildOps(query.collections, query.terms, '-')
1614

1715
/* handle combinations */
1816
if (ANDS && ORS) {
@@ -39,24 +37,19 @@ export function buildSearch(query: query): any {
3937
SORT TFIDF(doc) DESC`
4038
}
4139

42-
function buildOps(
43-
collections: collection[],
44-
terms: term[],
45-
op: string,
46-
key: string[],
47-
): any {
40+
function buildOps(collections: collection[], terms: term[], op: string): any {
4841
const opWord: string = op == '+' ? ' AND ' : ' OR '
4942

5043
let queryTerms: any = terms.filter((t: term) => t.op == op)
5144
if (!queryTerms.length) return
5245

5346
/* phrases */
5447
let phrases = queryTerms.filter((qT: term) => qT.type == 'phr')
55-
phrases = buildPhrases(phrases, collections, key, opWord)
48+
phrases = buildPhrases(phrases, collections, opWord)
5649

5750
/* tokens */
5851
let tokens = queryTerms.filter((qT: { type: string }) => qT.type === 'tok')
59-
tokens = tokens && buildTokens(tokens, collections, key)
52+
tokens = tokens && buildTokens(tokens, collections)
6053

6154
/* if (!phrases && !tokens) return */
6255
if (op == '-') return { phrases, tokens }
@@ -67,25 +60,20 @@ function buildOps(
6760
function buildPhrases(
6861
phrases: term[],
6962
collections: collection[],
70-
key: string[],
7163
opWord: string,
7264
): any {
7365
if (!phrases.length) return undefined
7466

7567
return aql.join(
76-
phrases.map((phrase: any) => buildPhrase(phrase, collections, key)),
68+
phrases.map((phrase: any) => buildPhrase(phrase, collections)),
7769
opWord,
7870
)
7971
}
8072

81-
function buildPhrase(
82-
phrase: term,
83-
collections: collection[],
84-
key: string[],
85-
): any {
73+
function buildPhrase(phrase: term, collections: collection[]): any {
8674
const phrases = []
87-
key.forEach((k: string) =>
88-
collections.forEach((coll) =>
75+
collections.forEach((coll) =>
76+
coll.keys.forEach((k: string) =>
8977
phrases.push(
9078
aql`PHRASE(doc.${k}, ${phrase.val.slice(1, -1)}, ${coll.analyzer})`,
9179
),
@@ -94,11 +82,7 @@ function buildPhrase(
9482
return aql`(${aql.join(phrases, ' OR ')})`
9583
}
9684

97-
function buildTokens(
98-
tokens: term[],
99-
collections: collection[],
100-
key: string[],
101-
): any {
85+
function buildTokens(tokens: term[], collections: collection[]): any {
10286
if (!tokens.length) return
10387

10488
const opWordMap = {
@@ -117,10 +101,10 @@ function buildTokens(
117101
tokens: term[],
118102
op: string,
119103
analyzer: string,
120-
key: string[],
104+
keys: string[],
121105
) => {
122106
return aql.join(
123-
key.map(
107+
keys.map(
124108
(k) => aql`
125109
ANALYZER(
126110
TOKENS(${tokens}, ${analyzer})
@@ -131,10 +115,10 @@ function buildTokens(
131115
}
132116

133117
let remapped = []
134-
collections.forEach((coll) => {
118+
collections.forEach((col) => {
135119
remapped.push(
136120
...Object.keys(mapped).map((op) =>
137-
makeTokenAnalyzers(mapped[op], op, coll.analyzer, key),
121+
makeTokenAnalyzers(mapped[op], op, col.analyzer, col.keys),
138122
),
139123
)
140124
})

tests/bool.ts

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('boolean search logic', () => {
2727
const links = {}
2828
links[collectionName] = {
2929
fields: {
30-
text: { analyzers: ['text_en'] },
30+
text_en: { analyzers: ['text_en'] },
3131
text_es: { analyzers: ['text_es'] },
3232
},
3333
}
@@ -45,7 +45,7 @@ describe('boolean search logic', () => {
4545
/* add documents */
4646
const docA = {
4747
title: 'doc A',
48-
text: 'words in text in document',
48+
text_en: 'words in text in document',
4949
text_es: 'palabras en texto en documento',
5050
}
5151
let insert: any = await db.query(
@@ -55,14 +55,14 @@ describe('boolean search logic', () => {
5555

5656
const docB = {
5757
title: 'doc B',
58-
text: 'sample word string to search across Alice Bob',
58+
text_en: 'sample word string to search across Alice Bob',
5959
text_es: 'cadena de palabras de muestra para buscar Alice Bob',
6060
}
6161
insert = await db.query(aql`INSERT ${docB} INTO ${collection} RETURN NEW`)
6262
expect(insert._result[0].title).to.equal('doc B')
6363

6464
const wait = (t: number) => new Promise((keep) => setTimeout(keep, t))
65-
await wait(1400)
65+
await wait(1600)
6666

6767
const getAllInViewQuery = aql`
6868
FOR doc in ${aql.literal(info.name)}
@@ -100,9 +100,10 @@ describe('boolean search logic', () => {
100100
},
101101
],
102102
terms: '',
103-
key: ['text'],
103+
key: ['text_en'],
104104
}
105105
let aqlQuery = buildAQL(query)
106+
/* expect(aqlQuery.query).to.equal('') */
106107
let cursor = await db.query(aqlQuery)
107108
let has = cursor.hasNext()
108109
expect(has).to.be.ok
@@ -111,7 +112,7 @@ describe('boolean search logic', () => {
111112
expect(result).to.have.length(2)
112113

113114
// pass multiple keys
114-
query.key = ['text', 'text_es']
115+
query.key = ['text_en', 'text_es']
115116
aqlQuery = buildAQL(query)
116117
cursor = await db.query(aqlQuery)
117118
has = cursor.hasNext()
@@ -133,19 +134,21 @@ describe('boolean search logic', () => {
133134
{
134135
name: collectionName,
135136
analyzer: 'text_en',
137+
keys: ['text_en'],
136138
},
137139
{
138140
name: collectionName,
139141
analyzer: 'text_es',
142+
keys: ['text_es'],
140143
},
141144
],
142145
/* should match both results */
143146
terms: '+"word"',
144-
key: ['text'],
147+
key: [],
145148
}
146149

147150
let aqlQuery = buildAQL(query)
148-
151+
/* expect(aqlQuery.query).to.equal('') */
149152
/* should match 2 documents */
150153
let cursor = await db.query(aqlQuery)
151154
expect(cursor.hasNext()).to.be.ok
@@ -155,7 +158,6 @@ describe('boolean search logic', () => {
155158
expect(cursor.hasNext()).to.not.be.ok
156159

157160
/* spanish */
158-
query.key = ['text_es']
159161
query.terms = '+"palabras"'
160162
aqlQuery = buildAQL(query)
161163

@@ -168,9 +170,28 @@ describe('boolean search logic', () => {
168170
expect(cursor.hasNext()).to.not.be.ok
169171

170172
/* english */
171-
query.key = ['text_en']
173+
query.key = ['text_en', 'text_es']
172174
query.terms = '+"in document"'
173175
aqlQuery = buildAQL(query)
176+
expect(aqlQuery.bindVars.value0).to.equal('text_en')
177+
expect(aqlQuery.bindVars.value1).to.equal('in document')
178+
expect(aqlQuery.bindVars.value2).to.equal('text_es')
179+
expect(aqlQuery.bindVars.value3).to.deep.equal({
180+
collections: query.collections.map((c) => c.name),
181+
})
182+
expect(aqlQuery.query).to.equal(`
183+
FOR doc IN ${view.name}
184+
185+
SEARCH
186+
(PHRASE(doc.@value0, @value1, @value0) OR PHRASE(doc.@value2, @value1, @value2))
187+
188+
189+
190+
OPTIONS @value3
191+
SORT TFIDF(doc) DESC
192+
193+
LIMIT @value4, @value5
194+
RETURN doc`)
174195

175196
/* should match 1 document */
176197
cursor = await db.query(aqlQuery)
@@ -203,18 +224,18 @@ describe('boolean search logic', () => {
203224
query.key = ['text', 'text_es']
204225
query.terms = '+"buscar"'
205226
let q = buildAQL(query)
206-
expect(q.query).to.deep.equal(`
227+
expect(q.query).to.equal(`
207228
FOR doc IN ${query.view}
208229
209230
SEARCH
210-
(PHRASE(doc.@value0, @value1, @value2) OR PHRASE(doc.@value3, @value1, @value2))
231+
(PHRASE(doc.@value0, @value1, @value0) OR PHRASE(doc.@value2, @value1, @value2))
211232
212233
213234
214-
OPTIONS @value4
235+
OPTIONS @value3
215236
SORT TFIDF(doc) DESC
216237
217-
LIMIT @value5, @value6
238+
LIMIT @value4, @value5
218239
RETURN doc`)
219240
cursor = await db.query(q)
220241
expect(cursor.hasNext()).to.be.ok
@@ -233,15 +254,17 @@ describe('boolean search logic', () => {
233254
{
234255
name: collectionName,
235256
analyzer: 'text_en',
257+
keys: ['text_en'],
236258
},
237259
{
238260
name: collectionName,
239261
analyzer: 'text_es',
262+
keys: ['text_es'],
240263
},
241264
],
242265
/* should match both results */
243266
terms: '+word',
244-
key: ['text'],
267+
key: ['text_en'],
245268
}
246269

247270
/* should bring back 2 document with default empty query string */
@@ -378,10 +401,12 @@ describe('boolean search logic', () => {
378401
{
379402
name: collectionName,
380403
analyzer: 'text_en',
404+
keys: ['text_en'],
381405
},
382406
{
383407
name: collectionName,
384408
analyzer: 'text_es',
409+
keys: ['text_es'],
385410
},
386411
],
387412
/* should bring back 1 result */
@@ -398,7 +423,7 @@ describe('boolean search logic', () => {
398423
expect(result[0].title).to.equal('doc A')
399424

400425
/* should bring back 1 result */
401-
query.terms = '"-cadena"'
426+
query.terms = '-"cadena"'
402427
aqlQuery = buildAQL(query)
403428
cursor = await db.query(aqlQuery)
404429
expect(cursor.hasNext()).to.be.ok

0 commit comments

Comments
 (0)