Skip to content

Commit 62b26a3

Browse files
committed
fix: bugs in phrase searches, all tests passing
1 parent 81003f9 commit 62b26a3

File tree

5 files changed

+65
-71
lines changed

5 files changed

+65
-71
lines changed

README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ phrase"` to `buildAQL`'s query object as the `term` key, will produce the
1717
following query:
1818

1919
```aql
20-
FOR doc IN search_view
21-
2220
SEARCH
2321
MIN_MATCH(
2422
ANALYZER(
@@ -28,9 +26,9 @@ FOR doc IN search_view
2826
ANALYZER(
2927
TOKENS(@value0, @value1)
3028
ALL IN doc.@value2, @value1),
31-
@value3) AND @value4) <--------------- @value4 here is the PHRASE search
29+
@value3) AND (PHRASE(doc.@value2, @value4, @value1)))
3230
33-
AND
31+
AND
3432
3533
MIN_MATCH(
3634
ANALYZER(
@@ -40,9 +38,6 @@ FOR doc IN search_view
4038
4139
OPTIONS @value6
4240
SORT TFIDF(doc) DESC
43-
44-
LIMIT @value7, @value8
45-
RETURN doc
4641
```
4742
This query will retrieve all documents that __include__ the term "mandatory"
4843
AND __do not include__ the term "exclude", AND whose ranking will be boosted by the

src/search.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ function buildOPS(collections: collection[], terms: term[], op: string, key:
6464
}
6565

6666
function buildPhrase(phrase: term, collections: collection[], key: string): any {
67-
return collections.map(coll => {
67+
const phrases = collections.map(coll => {
6868
return aql`PHRASE(doc.${key}, ${phrase.val.slice(1, -1)}, ${coll.analyzer})`
6969
})
70+
return aql`(${aql.join(phrases, ' OR ')})`
71+
7072
}
7173

7274
function buildTokens(tokens: term[], collections: collection[], key: string): any {

tests/bool.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ describe("boolean search logic", () => {
4242

4343
/* add documents */
4444
const docA = { title: 'doc A', text: 'words in text in document' }
45-
const docB = { title: 'doc B', text: 'sample word string to search across' }
4645
let insert: any = await db.query(aql`INSERT ${docA} INTO ${collection} RETURN NEW`)
4746
expect(insert._result[ 0 ].title).to.equal('doc A')
4847

48+
const docB = { title: 'doc B', text: 'sample word string to search across' }
4949
insert = await db.query(aql`INSERT ${docB} INTO ${collection} RETURN NEW`)
5050
expect(insert._result[ 0 ].title).to.equal('doc B')
5151

@@ -104,29 +104,33 @@ describe("boolean search logic", () => {
104104
const info = await view.get()
105105
expect(info.name).to.be.a('string')
106106

107-
108107
/* phrase */
109108
let query = {
110109
view: info.name,
111110
collections: [ {
112111
name: collectionName,
113112
analyzer: 'text_en'
114113
} ],
114+
/* should match both results */
115115
terms: '+"word"'
116116
}
117117
let aqlQuery = buildAQL(query)
118+
expect(Object.keys(aqlQuery.bindVars)).to.have.length(6)
118119

119-
expect(Object.keys(aqlQuery.bindVars)).to.have.length(4)
120-
120+
/* should match 2 documents */
121121
let cursor = await db.query(aqlQuery)
122122
expect(cursor.hasNext()).to.be.ok
123-
/* should have two results*/
124123
let result = await cursor.next()
125124
expect(cursor.hasNext()).to.be.ok
126125
await cursor.next()
127126
expect(cursor.hasNext()).to.not.be.ok
128127

129-
/* should only have one document */
128+
/* should match 0 documents */
129+
query.terms = '+"wyuxaouw"'
130+
cursor = await db.query(buildAQL(query))
131+
expect(cursor.hasNext()).to.not.be.ok
132+
133+
/* should match 1 document */
130134
query.terms = '+"in document"'
131135
cursor = await db.query(buildAQL(query))
132136
expect(cursor.hasNext()).to.be.ok
@@ -139,13 +143,13 @@ describe("boolean search logic", () => {
139143
expect(cursor.hasNext()).to.be.ok
140144
result = await cursor.all()
141145
expect(result).to.have.length(1)
142-
expect(result[ 0 ].title).to.equal('doc A')
146+
expect(result[ 0 ].title).to.equal('doc B')
143147
expect(cursor.hasNext()).to.not.be.ok
144148

145149

146150
query.terms = '+"nowhere found"'
147151
cursor = await db.query(buildAQL(query))
148-
expect(cursor.hasNext()).to.be.ok
152+
expect(cursor.hasNext()).to.not.be.ok
149153

150154
/* tokens */
151155
query = {
@@ -184,16 +188,16 @@ describe("boolean search logic", () => {
184188
name: collectionName,
185189
analyzer: 'text_en'
186190
} ],
187-
terms: '+mandatory -exclude ?"optional phrase"'
191+
terms: '?word'
188192
}
189193
const aqlQuery = buildAQL(query)
190194
const cursor = await db.query(aqlQuery)
191195

192196
let has = cursor.hasNext()
193197
expect(has).to.be.ok
194198

195-
let result = await cursor.next()
196-
expect(result).to.deep.equal({ a: 1 })
199+
let result = await cursor.all()
200+
expect(result).to.have.length(2)
197201
})
198202
})
199203
})

tests/index.ts

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,26 @@ import { expect } from 'chai'
33
import { buildAQL } from '../src/index'
44

55
describe('index.ts', () => {
6-
it('should export a function named buildAQL', () => {
7-
expect(buildAQL).to.exist
8-
expect(buildAQL).to.be.a('function')
9-
})
6+
it('should export a function named buildAQL', () => {
7+
expect(buildAQL).to.exist
8+
expect(buildAQL).to.be.a('function')
9+
})
1010

11-
it('should validate the query', () => {
12-
expect(() => buildAQL({ view: '', collections: [], terms: [] })).to.throw(/query.view must be a valid ArangoSearch View name/)
11+
it('should validate the query', () => {
12+
expect(() => buildAQL({ view: '', collections: [], terms: [] })).to.throw(/query.view must be a valid ArangoSearch View name/)
1313

14-
expect(() => buildAQL({ view: 'view', collections: [], terms: [] })).to.throw(/query.collections must have at least one name/)
15-
})
14+
expect(() => buildAQL({ view: 'view', collections: [], terms: [] })).to.throw(/query.collections must have at least one name/)
15+
})
1616
})
1717

1818
describe('buildAQL', () => {
19-
it(`should return an aql object
19+
it(`should return an aql object
2020
when an empty string is passed for query terms`, () => {
21-
let query = { view: 'view', collections: [{ name: 'coll', analyzer: 'analyzer' }], terms: '' }
22-
const builtAQL = buildAQL(query)
23-
expect(builtAQL).to.be.an('object')
21+
let query = { view: 'view', collections: [ { name: 'coll', analyzer: 'analyzer' } ], terms: '' }
22+
const builtAQL = buildAQL(query)
23+
expect(builtAQL).to.be.an('object')
2424

25-
expect(builtAQL.query).to.equal(`
25+
expect(builtAQL.query).to.equal(`
2626
FOR doc IN view
2727
2828
SEARCH
@@ -35,16 +35,16 @@ describe('buildAQL', () => {
3535
3636
LIMIT @value2, @value3
3737
RETURN doc`
38-
)
39-
})
38+
)
39+
})
4040

41-
it(`should return an aql object
41+
it(`should return an aql object
4242
when an empty array is passed for query terms`, () => {
43-
let query = { view: 'view', collections: [{ name: 'coll', analyzer: 'analyzer' }], terms: [] }
44-
const builtAQL = buildAQL(query)
45-
expect(builtAQL).to.be.an('object')
43+
let query = { view: 'view', collections: [ { name: 'coll', analyzer: 'analyzer' } ], terms: [] }
44+
const builtAQL = buildAQL(query)
45+
expect(builtAQL).to.be.an('object')
4646

47-
expect(builtAQL.query).to.equal(`
47+
expect(builtAQL.query).to.equal(`
4848
FOR doc IN view
4949
5050
SEARCH
@@ -57,30 +57,27 @@ describe('buildAQL', () => {
5757
5858
LIMIT @value2, @value3
5959
RETURN doc`
60-
)
61-
})
60+
)
61+
})
6262

63-
it(`should return an aql object
63+
it(`should return an aql object
6464
when a phrase string is passed for query terms`, () => {
65-
let query = { view: 'view', collections: [{ name: 'coll', analyzer: 'analyzer' }], terms: '"phrase search"' }
66-
const builtAQL = buildAQL(query)
67-
expect(builtAQL).to.be.an('object')
65+
let query = { view: 'view', collections: [ { name: 'coll', analyzer: 'analyzer' } ], terms: '"phrase search"' }
66+
const builtAQL = buildAQL(query)
67+
expect(builtAQL).to.be.an('object')
6868

69-
expect(builtAQL.query).to.equal(`
69+
expect(builtAQL.query).to.equal(`
7070
FOR doc IN view
7171
7272
SEARCH
7373
74-
@value0
74+
(PHRASE(doc.@value0, @value1, @value2))
7575
7676
77-
OPTIONS @value1
77+
OPTIONS @value3
7878
SORT TFIDF(doc) DESC
7979
80-
LIMIT @value2, @value3
80+
LIMIT @value4, @value5
8181
RETURN doc`)
82-
83-
expect(builtAQL.bindVars.value0[0].query).to.deep.equal('PHRASE(doc.@value0, @value1, @value2)')
84-
})
85-
82+
})
8683
})

tests/search.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,11 @@ describe('search.js', () => {
3232
expect(builtSearch.query).to.equal(`
3333
SEARCH
3434
35-
@value0
35+
(PHRASE(doc.@value0, @value1, @value2))
3636
3737
38-
OPTIONS @value1
38+
OPTIONS @value3
3939
SORT TFIDF(doc) DESC`)
40-
41-
expect(builtSearch.bindVars.value0[ 0 ].query).to.deep.equal('PHRASE(doc.@value0, @value1, @value2)')
4240
})
4341

4442
it(`should return an array of aql objects
@@ -47,35 +45,33 @@ describe('search.js', () => {
4745
const builtSearch = buildSearch(query)
4846

4947
expect(Object.keys(builtSearch.bindVars)).to.have.length(7)
50-
expect(builtSearch.bindVars.value0[ 0 ].query).to.equal('PHRASE(doc.@value0, @value1, @value2)')
51-
expect(builtSearch.bindVars.value1).to.deep.equal('token')
48+
expect(builtSearch.bindVars.value0).to.deep.equal('text')
49+
expect(builtSearch.bindVars.value1).to.deep.equal('query string')
5250
expect(builtSearch.bindVars.value2).to.deep.equal('text_en')
53-
expect(builtSearch.bindVars.value3).to.deep.equal('text')
5451
expect(builtSearch.bindVars.value4).to.deep.equal(1)
5552
expect(builtSearch.bindVars.value5).to.deep.equal('a')
5653
expect(builtSearch.bindVars.value6).to.deep.equal({ collections: [ query.collections[ 0 ].name ] })
5754
expect(builtSearch.query).to.equal(`
5855
SEARCH
59-
@value0 OR (@value0 AND MIN_MATCH(
56+
(PHRASE(doc.@value0, @value1, @value2)) OR ((PHRASE(doc.@value0, @value1, @value2)) AND MIN_MATCH(
6057
ANALYZER(
61-
TOKENS(@value1, @value2)
62-
ANY IN doc.@value3, @value2),
58+
TOKENS(@value3, @value2)
59+
ANY IN doc.@value0, @value2),
6360
@value4))
6461
6562
AND
6663
6764
MIN_MATCH(
6865
ANALYZER(
6966
TOKENS(@value5, @value2)
70-
NONE IN doc.@value3, @value2),
67+
NONE IN doc.@value0, @value2),
7168
@value4)
7269
7370
OPTIONS @value6
7471
SORT TFIDF(doc) DESC`)
7572
})
7673

77-
/* TODO basically passes but there's spacing issues around AND */
78-
it.skip('should return an array of aql objects', () => {
74+
it('should return an array of aql objects', () => {
7975
const query = { view: 'search_view', collections: [ { name: 'coll', analyzer: 'text_en' } ], terms: '+mandatory -exclude ?"optional phrase"' }
8076
const builtSearch = buildSearch(query)
8177
expect(builtSearch.query).to.equal(`
@@ -88,16 +84,16 @@ describe('search.js', () => {
8884
ANALYZER(
8985
TOKENS(@value0, @value1)
9086
ALL IN doc.@value2, @value1),
91-
@value3) AND @value4)
92-
93-
AND
94-
87+
@value3) AND (PHRASE(doc.@value2, @value4, @value1)))
88+
89+
AND
90+
9591
MIN_MATCH(
9692
ANALYZER(
9793
TOKENS(@value5, @value1)
9894
NONE IN doc.@value2, @value1),
9995
@value3)
100-
96+
10197
OPTIONS @value6
10298
SORT TFIDF(doc) DESC`)
10399
})

0 commit comments

Comments
 (0)