Phrase search vs. multi-lexeme tokens
Hackers,
I'm investigating the bug report [1] about the behavior of
websearch_to_tsquery() with quotes and multi-lexeme tokens. See the
example below.
# select to_tsvector('pg_class foo') @@ websearch_to_tsquery('"pg_class
foo"');
?column?
----------
f
So, tsvector doesn't match tsquery, when absolutely the same text was
put to the to_tsvector() and to the quotes of websearch_to_tsquery().
Looks wrong to me. Let's examine output of to_tsvector() and
websearch_to_tsquery().
# select to_tsvector('pg_class foo');
to_tsvector
--------------------------
'class':2 'foo':3 'pg':1
# select websearch_to_tsquery('"pg_class foo"');
websearch_to_tsquery
------------------------------
( 'pg' & 'class' ) <-> 'foo'
(1 row)
So, 'pg_class' token was split into two lexemes 'pg' and 'class'. But
the output websearch_to_tsquery() connects 'pg' and 'class' with &
operator. tsquery expects 'pg' and 'class' to be both neighbors of
'foo'. So, 'pg' and 'class' are expected to share the same position,
and that isn't true for tsvector. Let's see how phraseto_tsquery()
handles that.
# select to_tsvector('pg_class foo') @@ phraseto_tsquery('pg_class foo');
?column?
----------
t
# select phraseto_tsquery('pg_class foo');
phraseto_tsquery
----------------------------
'pg' <-> 'class' <-> 'foo'
phraseto_tsquery() connects all the lexemes with phrase operators and
everything works OK.
For me it's obvious that phraseto_tsquery() and websearch_to_tsquery()
with quotes should work the same way. Noticeably, current behavior of
websearch_to_tsquery() is recorded in the regression tests. So, it
might look that this behavior is intended, but it's too ridiculous and
I think the regression tests contain oversight as well.
I've prepared a fix, which doesn't break the fts parser abstractions
too much (attached patch), but I've faced another similar issue in
to_tsquery().
# select to_tsvector('pg_class foo') @@ to_tsquery('pg_class <-> foo');
?column?
----------
f
# select to_tsquery('pg_class <-> foo');
to_tsquery
------------------------------
( 'pg' & 'class' ) <-> 'foo'
I think if a user writes 'pg_class <-> foo', then it's expected to
match 'pg_class foo' independently on which lexemes 'pg_class' is
split into.
This issue looks like the much more complex design bug in phrase
search. Fixing this would require some kind of readahead or multipass
processing, because we don't know how to process 'pg_class' in
advance.
Is this really a design bug existing in phrase search from the
beginning. Or am I missing something?
Links
1. /messages/by-id/16592-70b110ff9731c07d@postgresql.org
------
Regards,
Alexander Korotkov
Attachments:
websearch_fix_p2.patchapplication/octet-stream; name=websearch_fix_p2.patchDownload
diff --git a/src/backend/tsearch/to_tsany.c b/src/backend/tsearch/to_tsany.c
index e7cd6264db2..f5aae2da385 100644
--- a/src/backend/tsearch/to_tsany.c
+++ b/src/backend/tsearch/to_tsany.c
@@ -19,11 +19,13 @@
#include "utils/builtins.h"
#include "utils/jsonfuncs.h"
+typedef int (*QOperatorCallback) (TSQueryParserState state);
typedef struct MorphOpaque
{
Oid cfg_id;
int qoperator; /* query operator */
+ QOperatorCallback qoperator_callback;
} MorphOpaque;
typedef struct TSVectorBuildState
@@ -511,7 +513,12 @@ pushval_morph(Datum opaque, TSQueryParserState state, char *strval, int lenval,
/* put placeholders for each missing stop word */
pushStop(state);
if (cntpos)
- pushOperator(state, data->qoperator, 1);
+ {
+ if (data->qoperator != 0)
+ pushOperator(state, data->qoperator, 1);
+ else
+ pushOperator(state, data->qoperator_callback(state), 1);
+ }
cntpos++;
pos++;
}
@@ -552,7 +559,10 @@ pushval_morph(Datum opaque, TSQueryParserState state, char *strval, int lenval,
if (cntpos)
{
/* distance may be useful */
- pushOperator(state, data->qoperator, 1);
+ if (data->qoperator != 0)
+ pushOperator(state, data->qoperator, 1);
+ else
+ pushOperator(state, data->qoperator_callback(state), 1);
}
cntpos++;
@@ -665,7 +675,8 @@ websearch_to_tsquery_byid(PG_FUNCTION_ARGS)
data.cfg_id = PG_GETARG_OID(0);
- data.qoperator = OP_AND;
+ data.qoperator = 0;
+ data.qoperator_callback = websearch_qoperator_callback;
query = parse_tsquery(text_to_cstring(in),
pushval_morph,
diff --git a/src/backend/utils/adt/tsquery.c b/src/backend/utils/adt/tsquery.c
index 092e8a130bf..eae76f9b2e1 100644
--- a/src/backend/utils/adt/tsquery.c
+++ b/src/backend/utils/adt/tsquery.c
@@ -520,6 +520,12 @@ gettoken_query_websearch(TSQueryParserState state, int8 *operator,
}
}
+int
+websearch_qoperator_callback(TSQueryParserState state)
+{
+ return state->in_quotes ? OP_PHRASE : OP_AND;
+}
+
static ts_tokentype
gettoken_query_plain(TSQueryParserState state, int8 *operator,
int *lenval, char **strval,
diff --git a/src/include/tsearch/ts_utils.h b/src/include/tsearch/ts_utils.h
index 400ba330014..be7b6b592f7 100644
--- a/src/include/tsearch/ts_utils.h
+++ b/src/include/tsearch/ts_utils.h
@@ -66,6 +66,8 @@ extern TSQuery parse_tsquery(char *buf,
Datum opaque,
int flags);
+extern int websearch_qoperator_callback(TSQueryParserState state);
+
/* Functions for use by PushFunction implementations */
extern void pushValue(TSQueryParserState state,
char *strval, int lenval, int16 weight, bool prefix);
diff --git a/src/test/regress/expected/tsearch.out b/src/test/regress/expected/tsearch.out
index 0110b4d2e0d..a158286454e 100644
--- a/src/test/regress/expected/tsearch.out
+++ b/src/test/regress/expected/tsearch.out
@@ -2690,33 +2690,33 @@ select websearch_to_tsquery('english', 'pg_class pg"');
(1 row)
select websearch_to_tsquery('english', '"pg_class pg"');
- websearch_to_tsquery
------------------------------
- ( 'pg' & 'class' ) <-> 'pg'
+ websearch_to_tsquery
+---------------------------
+ 'pg' <-> 'class' <-> 'pg'
(1 row)
select websearch_to_tsquery('english', 'abc "pg_class pg"');
- websearch_to_tsquery
--------------------------------------
- 'abc' & ( 'pg' & 'class' ) <-> 'pg'
+ websearch_to_tsquery
+-----------------------------------
+ 'abc' & 'pg' <-> 'class' <-> 'pg'
(1 row)
select websearch_to_tsquery('english', '"pg_class pg" def');
- websearch_to_tsquery
--------------------------------------
- ( 'pg' & 'class' ) <-> 'pg' & 'def'
+ websearch_to_tsquery
+-----------------------------------
+ 'pg' <-> 'class' <-> 'pg' & 'def'
(1 row)
select websearch_to_tsquery('english', 'abc "pg pg_class pg" def');
- websearch_to_tsquery
-------------------------------------------------------
- 'abc' & 'pg' <-> ( 'pg' & 'class' ) <-> 'pg' & 'def'
+ websearch_to_tsquery
+--------------------------------------------------------
+ 'abc' & 'pg' <-> ( 'pg' <-> 'class' ) <-> 'pg' & 'def'
(1 row)
select websearch_to_tsquery('english', ' or "pg pg_class pg" or ');
- websearch_to_tsquery
---------------------------------------
- 'pg' <-> ( 'pg' & 'class' ) <-> 'pg'
+ websearch_to_tsquery
+----------------------------------------
+ 'pg' <-> ( 'pg' <-> 'class' ) <-> 'pg'
(1 row)
select websearch_to_tsquery('english', '""pg pg_class pg""');
On Thu, Nov 12, 2020 at 4:09 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:
This issue looks like the much more complex design bug in phrase
search. Fixing this would require some kind of readahead or multipass
processing, because we don't know how to process 'pg_class' in
advance.Is this really a design bug existing in phrase search from the
beginning. Or am I missing something?
No feedback yet. I've added this to the commitfest to don't lose track of this.
https://commitfest.postgresql.org/31/2854/
------
Regards,
Alexander Korotkov
Alexander Korotkov <aekorotkov@gmail.com> writes:
# select to_tsvector('pg_class foo') @@ websearch_to_tsquery('"pg_class foo"');
?column?
----------
f
Yeah, surely this is wrong.
# select to_tsquery('pg_class <-> foo');
to_tsquery
------------------------------
( 'pg' & 'class' ) <-> 'foo'
I think if a user writes 'pg_class <-> foo', then it's expected to
match 'pg_class foo' independently on which lexemes 'pg_class' is
split into.
Indeed. It seems to me that this:
regression=# select to_tsquery('pg_class');
to_tsquery
----------------
'pg' & 'class'
(1 row)
is wrong all by itself. Now that we have phrase search, a much
saner translation would be "'pg' <-> 'class'". If we fixed that
then it seems like the more complex case would just work.
I read your patch over quickly and it seems like a reasonable
approach (but sadly underdocumented). Can we extend the idea
to fix the to_tsquery case?
regards, tom lane
Hi!
On Wed, Jan 6, 2021 at 8:18 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Alexander Korotkov <aekorotkov@gmail.com> writes:
# select to_tsvector('pg_class foo') @@ websearch_to_tsquery('"pg_class foo"');
?column?
----------
fYeah, surely this is wrong.
Thank you for confirming my thoughts. I also felt that is wrong but
doubted such a basic bug could exist for so long.
# select to_tsquery('pg_class <-> foo');
to_tsquery
------------------------------
( 'pg' & 'class' ) <-> 'foo'I think if a user writes 'pg_class <-> foo', then it's expected to
match 'pg_class foo' independently on which lexemes 'pg_class' is
split into.Indeed. It seems to me that this:
regression=# select to_tsquery('pg_class');
to_tsquery
----------------
'pg' & 'class'
(1 row)is wrong all by itself. Now that we have phrase search, a much
saner translation would be "'pg' <-> 'class'". If we fixed that
then it seems like the more complex case would just work.
Nice idea! Fixing this way should be much easier than fixing only the
case when we have the phrase operator on the upper level.
I read your patch over quickly and it seems like a reasonable
approach (but sadly underdocumented). Can we extend the idea
to fix the to_tsquery case?
Sure, I'll provide a revised patch.
------
Regards,
Alexander Korotkov
On Thu, Jan 7, 2021 at 6:36 AM Alexander Korotkov <aekorotkov@gmail.com> wrote:
I read your patch over quickly and it seems like a reasonable
approach (but sadly underdocumented). Can we extend the idea
to fix the to_tsquery case?Sure, I'll provide a revised patch.
The next version of the patch is attached. Now, it just makes
to_tsquery() and websearch_to_tsquery() use phrase operator to connect
multiple lexemes of the same tsquery token. I leave plainto_tsquery()
aside because it considers the whole argument as a single token.
Changing it would make it an equivalent of phraseto_tsquery().
------
Regards,
Alexander Korotkov
Attachments:
tsquery_phrase_fix.pathapplication/octet-stream; name=tsquery_phrase_fix.pathDownload
commit 278d9b7aa45478bb0433eb2f15ab47910c65d09c
Author: Alexander Korotkov <akorotkov@postgresql.org>
Date: Mon Jan 18 00:00:38 2021 +0300
Initial.
Reported-by:
Bug:
Discussion:
Author:
Reviewed-by:
Tested-by:
Backpatch-through:
diff --git a/src/backend/tsearch/to_tsany.c b/src/backend/tsearch/to_tsany.c
index e7cd6264db2..9fea37d76a3 100644
--- a/src/backend/tsearch/to_tsany.c
+++ b/src/backend/tsearch/to_tsany.c
@@ -573,7 +573,7 @@ to_tsquery_byid(PG_FUNCTION_ARGS)
MorphOpaque data;
data.cfg_id = PG_GETARG_OID(0);
- data.qoperator = OP_AND;
+ data.qoperator = OP_PHRASE;
query = parse_tsquery(text_to_cstring(in),
pushval_morph,
@@ -665,7 +665,7 @@ websearch_to_tsquery_byid(PG_FUNCTION_ARGS)
data.cfg_id = PG_GETARG_OID(0);
- data.qoperator = OP_AND;
+ data.qoperator = OP_PHRASE;
query = parse_tsquery(text_to_cstring(in),
pushval_morph,
diff --git a/src/test/regress/sql/tsearch.sql b/src/test/regress/sql/tsearch.sql
index 8a27fcd8b0b..b02ed73f6a8 100644
--- a/src/test/regress/sql/tsearch.sql
+++ b/src/test/regress/sql/tsearch.sql
@@ -554,10 +554,10 @@ to_tsquery('english','Lorem') && phraseto_tsquery('english','ullamcorper urna'),
CREATE TABLE test_tsquery (txtkeyword TEXT, txtsample TEXT);
\set ECHO none
\copy test_tsquery from stdin
-'New York' new & york | big & apple | nyc
+'New York' new <-> york | big <-> apple | nyc
Moscow moskva | moscow
'Sanct Peter' Peterburg | peter | 'Sanct Peterburg'
-'foo bar qq' foo & (bar | qq) & city
+foo & bar & qq foo & (bar | qq) & city
1 & (2 <-> 3) 2 <-> 4
5 <-> 6 5 <-> 7
\.
@@ -569,21 +569,21 @@ ALTER TABLE test_tsquery ADD COLUMN sample tsquery;
UPDATE test_tsquery SET sample = to_tsquery('english', txtsample::text);
-SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new <-> york';
CREATE UNIQUE INDEX bt_tsq ON test_tsquery (keyword);
SET enable_seqscan=OFF;
-SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new <-> york';
RESET enable_seqscan;
@@ -593,11 +593,11 @@ SELECT ts_rewrite(ts_rewrite('new & !york ', 'york', '!jersey'),
SELECT ts_rewrite('moscow', 'SELECT keyword, sample FROM test_tsquery'::text );
SELECT ts_rewrite('moscow & hotel', 'SELECT keyword, sample FROM test_tsquery'::text );
-SELECT ts_rewrite('bar & new & qq & foo & york', 'SELECT keyword, sample FROM test_tsquery'::text );
+SELECT ts_rewrite('bar & qq & foo & (new <-> york)', 'SELECT keyword, sample FROM test_tsquery'::text );
SELECT ts_rewrite( 'moscow', 'SELECT keyword, sample FROM test_tsquery');
SELECT ts_rewrite( 'moscow & hotel', 'SELECT keyword, sample FROM test_tsquery');
-SELECT ts_rewrite( 'bar & new & qq & foo & york', 'SELECT keyword, sample FROM test_tsquery');
+SELECT ts_rewrite( 'bar & qq & foo & (new <-> york)', 'SELECT keyword, sample FROM test_tsquery');
SELECT ts_rewrite('1 & (2 <-> 3)', 'SELECT keyword, sample FROM test_tsquery'::text );
SELECT ts_rewrite('1 & (2 <2> 3)', 'SELECT keyword, sample FROM test_tsquery'::text );
@@ -614,10 +614,10 @@ SELECT keyword FROM test_tsquery WHERE keyword <@ 'new';
SELECT keyword FROM test_tsquery WHERE keyword <@ 'moscow';
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow & hotel') AS query;
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow & hotel') AS query;
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
CREATE INDEX qq ON test_tsquery USING gist (keyword tsquery_ops);
SET enable_seqscan=OFF;
@@ -628,10 +628,10 @@ SELECT keyword FROM test_tsquery WHERE keyword <@ 'new';
SELECT keyword FROM test_tsquery WHERE keyword <@ 'moscow';
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow & hotel') AS query;
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow & hotel') AS query;
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
SELECT ts_rewrite(tsquery_phrase('foo', 'foo'), 'foo', 'bar | baz');
SELECT to_tsvector('foo bar') @@
The following review has been posted through the commitfest application:
make installcheck-world: tested, passed
Implements feature: tested, passed
Spec compliant: not tested
Documentation: not tested
Greetings,
Although I am not an expert in this field, I carefully read the full-text search section in the document. I think the change is surprising, but yes, it is correct.
I found that your patch didn't modify the regress/excepted/tsearch.out. So I updated it and carried out the regression test. It passed. Also, I manually executed some test cases, all of which were OK.
Hi, Neil!
On Mon, Jan 25, 2021 at 11:45 AM Neil Chen <carpenter.nail.cz@gmail.com> wrote:
The following review has been posted through the commitfest application:
make installcheck-world: tested, passed
Implements feature: tested, passed
Spec compliant: not tested
Documentation: not testedGreetings,
Although I am not an expert in this field, I carefully read the full-text search section in the document. I think the change is surprising, but yes, it is correct.
I found that your patch didn't modify the regress/excepted/tsearch.out. So I updated it and carried out the regression test. It passed. Also, I manually executed some test cases, all of which were OK.
Thank you for looking into this. Yes, I've adjusted tsearch.sql
regression tests to provide reasonable exercises for the new logic,
but forgot to add tsearch.out to the patch.
BTW, you mentioned you read the documentation. Do you think it needs
to be adjusted accordingly to the patch?
------
Regards,
Alexander Korotkov
Hi Alexander,
On Mon, Jan 25, 2021 at 11:25 PM Alexander Korotkov <aekorotkov@gmail.com>
wrote:
BTW, you mentioned you read the documentation. Do you think it needs
to be adjusted accordingly to the patch?
Yes, I checked section 8.11, section 9.13 and Chapter 12 of the document.
The change of this patch did not conflict with the document, because it was
not mentioned in the document at all. We can simply not modify it, or we
can supplement these situations.
--
There is no royal road to learning.
HighGo Software Co.
On Tue, Jan 26, 2021 at 4:31 AM Neil Chen <carpenter.nail.cz@gmail.com> wrote:
On Mon, Jan 25, 2021 at 11:25 PM Alexander Korotkov <aekorotkov@gmail.com> wrote:
BTW, you mentioned you read the documentation. Do you think it needs
to be adjusted accordingly to the patch?Yes, I checked section 8.11, section 9.13 and Chapter 12 of the document. The change of this patch did not conflict with the document, because it was not mentioned in the document at all. We can simply not modify it, or we can supplement these situations.
I've checked the docs myself and I think you're right (despite that's
surprising for me). It seems that this patch just changes
undocumented aspects of full-text search to be more consistent and
intuitive.
The revised patch is attached. This revision adds just comment and
commit message. I'm going to push this if no objections.
------
Regards,
Alexander Korotkov
Attachments:
tsquery_phrase_fix_v2.patchapplication/octet-stream; name=tsquery_phrase_fix_v2.patchDownload
commit 296f01b1c2fe92aa834b7f199155e41618d122d0
Author: Alexander Korotkov <akorotkov@postgresql.org>
Date: Fri Jan 29 16:49:34 2021 +0300
Fix parsing of complex morphs to tsquery
When to_tsquery() or websearch_to_tsquery() meet a complex morph containing
multiple words residing adjacent position, these words are connected
with OP_AND operator. That leads to surprising results. For instace,
both websearch_to_tsquery('"pg_class pg"') and to_tsquery('pg_class <-> pg')
produce '( pg & class ) <-> pg' tsquery. This tsquery requires
'pg' and 'class' words to reside on the same position and doesn't match
to to_tsvector('pg_class pg'). It appears to be ridiculous behavior, which
needs to be fixed.
This commit makes to_tsquery() or websearch_to_tsquery() connect words
residing adjacent position with OP_PHRASE. Therefore, now those words are
normally chained with other OP_PHRASE operator. The examples of above now
produces 'pg <-> class <-> pg' tsquery, which matches to
to_tsvector('pg_class pg').
Another effect of this commit is that complex morph word positions now need to
match the tsvector even if there is no surrounding OP_PHRASE. This behavior
change generally looks like an improvement but making this commit not
backpatchable.
Reported-by: Barry Pederson
Bug: #16592
Discussion: https://postgr.es/m/16592-70b110ff9731c07d@postgresql.org
Discussion: https://postgr.es/m/CAPpHfdv0EzVhf6CWfB1_TTZqXV_2Sn-jSY3zSd7ePH%3D-%2B1V2DQ%40mail.gmail.com
Author: Alexander Korotkov
Reviewed-by: Tom Lane, Neil Chen
diff --git a/src/backend/tsearch/to_tsany.c b/src/backend/tsearch/to_tsany.c
index e7cd6264db2..3cfd3e01c8c 100644
--- a/src/backend/tsearch/to_tsany.c
+++ b/src/backend/tsearch/to_tsany.c
@@ -20,10 +20,20 @@
#include "utils/jsonfuncs.h"
+/*
+ * Opaque data structure, which is passed by parse_tsquery() to pushval_morph().
+ */
typedef struct MorphOpaque
{
Oid cfg_id;
- int qoperator; /* query operator */
+
+ /*
+ * Single tsquery morph could be parsed into multiple words. When these
+ * words reside in adjacent positions, they are connected using this
+ * operator. Usually, that is OP_PHRASE, which requires word positions of
+ * a complex morph to exactly match the tsvector.
+ */
+ int qoperator;
} MorphOpaque;
typedef struct TSVectorBuildState
@@ -573,7 +583,14 @@ to_tsquery_byid(PG_FUNCTION_ARGS)
MorphOpaque data;
data.cfg_id = PG_GETARG_OID(0);
- data.qoperator = OP_AND;
+
+ /*
+ * Passing OP_PHRASE as a qoperator makes tsquery require matching of word
+ * positions of a complex morph exactly match the tsvector. Also, when
+ * the complex morphs are connected with OP_PHRASE operator, we connect
+ * all their words into the OP_PHRASE sequence.
+ */
+ data.qoperator = OP_PHRASE;
query = parse_tsquery(text_to_cstring(in),
pushval_morph,
@@ -603,6 +620,12 @@ plainto_tsquery_byid(PG_FUNCTION_ARGS)
MorphOpaque data;
data.cfg_id = PG_GETARG_OID(0);
+
+ /*
+ * parse_tsquery() with P_TSQ_PLAIN flag takes the whole input text as a
+ * single morph. Passing OP_PHRASE as a qoperator makes tsquery require
+ * matching of all words independently on their positions.
+ */
data.qoperator = OP_AND;
query = parse_tsquery(text_to_cstring(in),
@@ -634,6 +657,12 @@ phraseto_tsquery_byid(PG_FUNCTION_ARGS)
MorphOpaque data;
data.cfg_id = PG_GETARG_OID(0);
+
+ /*
+ * parse_tsquery() with P_TSQ_PLAIN flag takes the whole input text as a
+ * single morph. Passing OP_PHRASE as a qoperator makes tsquery require
+ * matching of word positions.
+ */
data.qoperator = OP_PHRASE;
query = parse_tsquery(text_to_cstring(in),
@@ -665,7 +694,13 @@ websearch_to_tsquery_byid(PG_FUNCTION_ARGS)
data.cfg_id = PG_GETARG_OID(0);
- data.qoperator = OP_AND;
+ /*
+ * Passing OP_PHRASE as a qoperator makes tsquery require matching of word
+ * positions of a complex morph exactly match the tsvector. Also, when
+ * the complex morphs are given in quotes, we connect all their words into
+ * the OP_PHRASE sequence.
+ */
+ data.qoperator = OP_PHRASE;
query = parse_tsquery(text_to_cstring(in),
pushval_morph,
diff --git a/src/test/regress/expected/tsearch.out b/src/test/regress/expected/tsearch.out
index 0110b4d2e0d..4ae62320c9f 100644
--- a/src/test/regress/expected/tsearch.out
+++ b/src/test/regress/expected/tsearch.out
@@ -1997,31 +1997,31 @@ ALTER TABLE test_tsquery ADD COLUMN keyword tsquery;
UPDATE test_tsquery SET keyword = to_tsquery('english', txtkeyword);
ALTER TABLE test_tsquery ADD COLUMN sample tsquery;
UPDATE test_tsquery SET sample = to_tsquery('english', txtsample::text);
-SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new <-> york';
count
-------
2
(1 row)
-SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new <-> york';
count
-------
3
(1 row)
-SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new <-> york';
count
-------
1
(1 row)
-SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new <-> york';
count
-------
4
(1 row)
-SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new <-> york';
count
-------
3
@@ -2029,31 +2029,31 @@ SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new & york';
CREATE UNIQUE INDEX bt_tsq ON test_tsquery (keyword);
SET enable_seqscan=OFF;
-SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new <-> york';
count
-------
2
(1 row)
-SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new <-> york';
count
-------
3
(1 row)
-SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new <-> york';
count
-------
1
(1 row)
-SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new <-> york';
count
-------
4
(1 row)
-SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new <-> york';
count
-------
3
@@ -2085,10 +2085,10 @@ SELECT ts_rewrite('moscow & hotel', 'SELECT keyword, sample FROM test_tsquery'::
'hotel' & ( 'moskva' | 'moscow' )
(1 row)
-SELECT ts_rewrite('bar & new & qq & foo & york', 'SELECT keyword, sample FROM test_tsquery'::text );
- ts_rewrite
----------------------------------------------------------------------------------
- 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' & 'appl' | 'new' & 'york' )
+SELECT ts_rewrite('bar & qq & foo & (new <-> york)', 'SELECT keyword, sample FROM test_tsquery'::text );
+ ts_rewrite
+-------------------------------------------------------------------------------------
+ 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' <-> 'appl' | 'new' <-> 'york' )
(1 row)
SELECT ts_rewrite( 'moscow', 'SELECT keyword, sample FROM test_tsquery');
@@ -2103,10 +2103,10 @@ SELECT ts_rewrite( 'moscow & hotel', 'SELECT keyword, sample FROM test_tsquery')
'hotel' & ( 'moskva' | 'moscow' )
(1 row)
-SELECT ts_rewrite( 'bar & new & qq & foo & york', 'SELECT keyword, sample FROM test_tsquery');
- ts_rewrite
----------------------------------------------------------------------------------
- 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' & 'appl' | 'new' & 'york' )
+SELECT ts_rewrite( 'bar & qq & foo & (new <-> york)', 'SELECT keyword, sample FROM test_tsquery');
+ ts_rewrite
+-------------------------------------------------------------------------------------
+ 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' <-> 'appl' | 'new' <-> 'york' )
(1 row)
SELECT ts_rewrite('1 & (2 <-> 3)', 'SELECT keyword, sample FROM test_tsquery'::text );
@@ -2149,9 +2149,9 @@ NOTICE: text-search query doesn't contain lexemes: ""
(1 row)
SELECT keyword FROM test_tsquery WHERE keyword @> 'new';
- keyword
-----------------
- 'new' & 'york'
+ keyword
+------------------
+ 'new' <-> 'york'
(1 row)
SELECT keyword FROM test_tsquery WHERE keyword @> 'moscow';
@@ -2183,10 +2183,10 @@ SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_t
'hotel' & ( 'moskva' | 'moscow' )
(1 row)
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
- ts_rewrite
----------------------------------------------------------------------------------
- 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' & 'appl' | 'new' & 'york' )
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
+ ts_rewrite
+-------------------------------------------------------------------------------------
+ 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' <-> 'appl' | 'new' <-> 'york' )
(1 row)
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
@@ -2201,18 +2201,18 @@ SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_t
'hotel' & ( 'moskva' | 'moscow' )
(1 row)
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
- ts_rewrite
----------------------------------------------------------------------------------
- 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' & 'appl' | 'new' & 'york' )
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
+ ts_rewrite
+-------------------------------------------------------------------------------------
+ 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' <-> 'appl' | 'new' <-> 'york' )
(1 row)
CREATE INDEX qq ON test_tsquery USING gist (keyword tsquery_ops);
SET enable_seqscan=OFF;
SELECT keyword FROM test_tsquery WHERE keyword @> 'new';
- keyword
-----------------
- 'new' & 'york'
+ keyword
+------------------
+ 'new' <-> 'york'
(1 row)
SELECT keyword FROM test_tsquery WHERE keyword @> 'moscow';
@@ -2244,10 +2244,10 @@ SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_t
'hotel' & ( 'moskva' | 'moscow' )
(1 row)
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
- ts_rewrite
----------------------------------------------------------------------------------
- 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' & 'appl' | 'new' & 'york' )
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
+ ts_rewrite
+-------------------------------------------------------------------------------------
+ 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' <-> 'appl' | 'new' <-> 'york' )
(1 row)
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
@@ -2262,10 +2262,10 @@ SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_t
'hotel' & ( 'moskva' | 'moscow' )
(1 row)
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
- ts_rewrite
----------------------------------------------------------------------------------
- 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' & 'appl' | 'new' & 'york' )
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
+ ts_rewrite
+-------------------------------------------------------------------------------------
+ 'citi' & 'foo' & ( 'bar' | 'qq' ) & ( 'nyc' | 'big' <-> 'appl' | 'new' <-> 'york' )
(1 row)
SELECT ts_rewrite(tsquery_phrase('foo', 'foo'), 'foo', 'bar | baz');
@@ -2456,19 +2456,19 @@ select websearch_to_tsquery('simple', 'fat:A : cat:B');
select websearch_to_tsquery('simple', 'fat*rat');
websearch_to_tsquery
----------------------
- 'fat' & 'rat'
+ 'fat' <-> 'rat'
(1 row)
select websearch_to_tsquery('simple', 'fat-rat');
- websearch_to_tsquery
----------------------------
- 'fat-rat' & 'fat' & 'rat'
+ websearch_to_tsquery
+-------------------------------
+ 'fat-rat' <-> 'fat' <-> 'rat'
(1 row)
select websearch_to_tsquery('simple', 'fat_rat');
websearch_to_tsquery
----------------------
- 'fat' & 'rat'
+ 'fat' <-> 'rat'
(1 row)
-- weights are completely ignored
@@ -2665,64 +2665,64 @@ select websearch_to_tsquery('simple', 'abc OR1234');
(1 row)
select websearch_to_tsquery('simple', 'abc or-abc');
- websearch_to_tsquery
----------------------------------
- 'abc' & 'or-abc' & 'or' & 'abc'
+ websearch_to_tsquery
+-------------------------------------
+ 'abc' & 'or-abc' <-> 'or' <-> 'abc'
(1 row)
select websearch_to_tsquery('simple', 'abc OR_abc');
- websearch_to_tsquery
-----------------------
- 'abc' & 'or' & 'abc'
+ websearch_to_tsquery
+------------------------
+ 'abc' & 'or' <-> 'abc'
(1 row)
-- test quotes
select websearch_to_tsquery('english', '"pg_class pg');
- websearch_to_tsquery
------------------------
- 'pg' & 'class' & 'pg'
+ websearch_to_tsquery
+-------------------------
+ 'pg' <-> 'class' & 'pg'
(1 row)
select websearch_to_tsquery('english', 'pg_class pg"');
- websearch_to_tsquery
------------------------
- 'pg' & 'class' & 'pg'
+ websearch_to_tsquery
+-------------------------
+ 'pg' <-> 'class' & 'pg'
(1 row)
select websearch_to_tsquery('english', '"pg_class pg"');
- websearch_to_tsquery
------------------------------
- ( 'pg' & 'class' ) <-> 'pg'
+ websearch_to_tsquery
+---------------------------
+ 'pg' <-> 'class' <-> 'pg'
(1 row)
select websearch_to_tsquery('english', 'abc "pg_class pg"');
- websearch_to_tsquery
--------------------------------------
- 'abc' & ( 'pg' & 'class' ) <-> 'pg'
+ websearch_to_tsquery
+-----------------------------------
+ 'abc' & 'pg' <-> 'class' <-> 'pg'
(1 row)
select websearch_to_tsquery('english', '"pg_class pg" def');
- websearch_to_tsquery
--------------------------------------
- ( 'pg' & 'class' ) <-> 'pg' & 'def'
+ websearch_to_tsquery
+-----------------------------------
+ 'pg' <-> 'class' <-> 'pg' & 'def'
(1 row)
select websearch_to_tsquery('english', 'abc "pg pg_class pg" def');
- websearch_to_tsquery
-------------------------------------------------------
- 'abc' & 'pg' <-> ( 'pg' & 'class' ) <-> 'pg' & 'def'
+ websearch_to_tsquery
+--------------------------------------------------------
+ 'abc' & 'pg' <-> ( 'pg' <-> 'class' ) <-> 'pg' & 'def'
(1 row)
select websearch_to_tsquery('english', ' or "pg pg_class pg" or ');
- websearch_to_tsquery
---------------------------------------
- 'pg' <-> ( 'pg' & 'class' ) <-> 'pg'
+ websearch_to_tsquery
+----------------------------------------
+ 'pg' <-> ( 'pg' <-> 'class' ) <-> 'pg'
(1 row)
select websearch_to_tsquery('english', '""pg pg_class pg""');
- websearch_to_tsquery
-------------------------------
- 'pg' & 'pg' & 'class' & 'pg'
+ websearch_to_tsquery
+--------------------------------
+ 'pg' & 'pg' <-> 'class' & 'pg'
(1 row)
select websearch_to_tsquery('english', 'abc """"" def');
@@ -2829,7 +2829,7 @@ NOTICE: text-search query contains only stop words or doesn't contain lexemes,
select websearch_to_tsquery('''abc''''def''');
websearch_to_tsquery
----------------------
- 'abc' & 'def'
+ 'abc' <-> 'def'
(1 row)
select websearch_to_tsquery('\abc');
diff --git a/src/test/regress/sql/tsearch.sql b/src/test/regress/sql/tsearch.sql
index 8a27fcd8b0b..b02ed73f6a8 100644
--- a/src/test/regress/sql/tsearch.sql
+++ b/src/test/regress/sql/tsearch.sql
@@ -554,10 +554,10 @@ to_tsquery('english','Lorem') && phraseto_tsquery('english','ullamcorper urna'),
CREATE TABLE test_tsquery (txtkeyword TEXT, txtsample TEXT);
\set ECHO none
\copy test_tsquery from stdin
-'New York' new & york | big & apple | nyc
+'New York' new <-> york | big <-> apple | nyc
Moscow moskva | moscow
'Sanct Peter' Peterburg | peter | 'Sanct Peterburg'
-'foo bar qq' foo & (bar | qq) & city
+foo & bar & qq foo & (bar | qq) & city
1 & (2 <-> 3) 2 <-> 4
5 <-> 6 5 <-> 7
\.
@@ -569,21 +569,21 @@ ALTER TABLE test_tsquery ADD COLUMN sample tsquery;
UPDATE test_tsquery SET sample = to_tsquery('english', txtsample::text);
-SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new <-> york';
CREATE UNIQUE INDEX bt_tsq ON test_tsquery (keyword);
SET enable_seqscan=OFF;
-SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new & york';
-SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new & york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword < 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword <= 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword = 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword >= 'new <-> york';
+SELECT COUNT(*) FROM test_tsquery WHERE keyword > 'new <-> york';
RESET enable_seqscan;
@@ -593,11 +593,11 @@ SELECT ts_rewrite(ts_rewrite('new & !york ', 'york', '!jersey'),
SELECT ts_rewrite('moscow', 'SELECT keyword, sample FROM test_tsquery'::text );
SELECT ts_rewrite('moscow & hotel', 'SELECT keyword, sample FROM test_tsquery'::text );
-SELECT ts_rewrite('bar & new & qq & foo & york', 'SELECT keyword, sample FROM test_tsquery'::text );
+SELECT ts_rewrite('bar & qq & foo & (new <-> york)', 'SELECT keyword, sample FROM test_tsquery'::text );
SELECT ts_rewrite( 'moscow', 'SELECT keyword, sample FROM test_tsquery');
SELECT ts_rewrite( 'moscow & hotel', 'SELECT keyword, sample FROM test_tsquery');
-SELECT ts_rewrite( 'bar & new & qq & foo & york', 'SELECT keyword, sample FROM test_tsquery');
+SELECT ts_rewrite( 'bar & qq & foo & (new <-> york)', 'SELECT keyword, sample FROM test_tsquery');
SELECT ts_rewrite('1 & (2 <-> 3)', 'SELECT keyword, sample FROM test_tsquery'::text );
SELECT ts_rewrite('1 & (2 <2> 3)', 'SELECT keyword, sample FROM test_tsquery'::text );
@@ -614,10 +614,10 @@ SELECT keyword FROM test_tsquery WHERE keyword <@ 'new';
SELECT keyword FROM test_tsquery WHERE keyword <@ 'moscow';
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow & hotel') AS query;
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow & hotel') AS query;
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
CREATE INDEX qq ON test_tsquery USING gist (keyword tsquery_ops);
SET enable_seqscan=OFF;
@@ -628,10 +628,10 @@ SELECT keyword FROM test_tsquery WHERE keyword <@ 'new';
SELECT keyword FROM test_tsquery WHERE keyword <@ 'moscow';
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow & hotel') AS query;
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow') AS query;
SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'moscow & hotel') AS query;
-SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & new & qq & foo & york') AS query;
+SELECT ts_rewrite( query, 'SELECT keyword, sample FROM test_tsquery' ) FROM to_tsquery('english', 'bar & qq & foo & (new <-> york)') AS query;
SELECT ts_rewrite(tsquery_phrase('foo', 'foo'), 'foo', 'bar | baz');
SELECT to_tsvector('foo bar') @@