1 /*-------------------------------------------------------------------------
2 *
3 * to_tsany.c
4 * to_ts* function definitions
5 *
6 * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group
7 *
8 *
9 * IDENTIFICATION
10 * src/backend/tsearch/to_tsany.c
11 *
12 *-------------------------------------------------------------------------
13 */
14 #include "postgres.h"
15
16 #include "common/jsonapi.h"
17 #include "tsearch/ts_cache.h"
18 #include "tsearch/ts_utils.h"
19 #include "utils/builtins.h"
20 #include "utils/jsonfuncs.h"
21
22
23 /*
24 * Opaque data structure, which is passed by parse_tsquery() to pushval_morph().
25 */
26 typedef struct MorphOpaque
27 {
28 Oid cfg_id;
29
30 /*
31 * Single tsquery morph could be parsed into multiple words. When these
32 * words reside in adjacent positions, they are connected using this
33 * operator. Usually, that is OP_PHRASE, which requires word positions of
34 * a complex morph to exactly match the tsvector.
35 */
36 int qoperator;
37 } MorphOpaque;
38
39 typedef struct TSVectorBuildState
40 {
41 ParsedText *prs;
42 Oid cfgId;
43 } TSVectorBuildState;
44
45 static void add_to_tsvector(void *_state, char *elem_value, int elem_len);
46
47
48 Datum
get_current_ts_config(PG_FUNCTION_ARGS)49 get_current_ts_config(PG_FUNCTION_ARGS)
50 {
51 PG_RETURN_OID(getTSCurrentConfig(true));
52 }
53
54 /*
55 * to_tsvector
56 */
57 static int
compareWORD(const void * a,const void * b)58 compareWORD(const void *a, const void *b)
59 {
60 int res;
61
62 res = tsCompareString(((const ParsedWord *) a)->word, ((const ParsedWord *) a)->len,
63 ((const ParsedWord *) b)->word, ((const ParsedWord *) b)->len,
64 false);
65
66 if (res == 0)
67 {
68 if (((const ParsedWord *) a)->pos.pos == ((const ParsedWord *) b)->pos.pos)
69 return 0;
70
71 res = (((const ParsedWord *) a)->pos.pos > ((const ParsedWord *) b)->pos.pos) ? 1 : -1;
72 }
73
74 return res;
75 }
76
77 static int
uniqueWORD(ParsedWord * a,int32 l)78 uniqueWORD(ParsedWord *a, int32 l)
79 {
80 ParsedWord *ptr,
81 *res;
82 int tmppos;
83
84 if (l == 1)
85 {
86 tmppos = LIMITPOS(a->pos.pos);
87 a->alen = 2;
88 a->pos.apos = (uint16 *) palloc(sizeof(uint16) * a->alen);
89 a->pos.apos[0] = 1;
90 a->pos.apos[1] = tmppos;
91 return l;
92 }
93
94 res = a;
95 ptr = a + 1;
96
97 /*
98 * Sort words with its positions
99 */
100 qsort((void *) a, l, sizeof(ParsedWord), compareWORD);
101
102 /*
103 * Initialize first word and its first position
104 */
105 tmppos = LIMITPOS(a->pos.pos);
106 a->alen = 2;
107 a->pos.apos = (uint16 *) palloc(sizeof(uint16) * a->alen);
108 a->pos.apos[0] = 1;
109 a->pos.apos[1] = tmppos;
110
111 /*
112 * Summarize position information for each word
113 */
114 while (ptr - a < l)
115 {
116 if (!(ptr->len == res->len &&
117 strncmp(ptr->word, res->word, res->len) == 0))
118 {
119 /*
120 * Got a new word, so put it in result
121 */
122 res++;
123 res->len = ptr->len;
124 res->word = ptr->word;
125 tmppos = LIMITPOS(ptr->pos.pos);
126 res->alen = 2;
127 res->pos.apos = (uint16 *) palloc(sizeof(uint16) * res->alen);
128 res->pos.apos[0] = 1;
129 res->pos.apos[1] = tmppos;
130 }
131 else
132 {
133 /*
134 * The word already exists, so adjust position information. But
135 * before we should check size of position's array, max allowed
136 * value for position and uniqueness of position
137 */
138 pfree(ptr->word);
139 if (res->pos.apos[0] < MAXNUMPOS - 1 && res->pos.apos[res->pos.apos[0]] != MAXENTRYPOS - 1 &&
140 res->pos.apos[res->pos.apos[0]] != LIMITPOS(ptr->pos.pos))
141 {
142 if (res->pos.apos[0] + 1 >= res->alen)
143 {
144 res->alen *= 2;
145 res->pos.apos = (uint16 *) repalloc(res->pos.apos, sizeof(uint16) * res->alen);
146 }
147 if (res->pos.apos[0] == 0 || res->pos.apos[res->pos.apos[0]] != LIMITPOS(ptr->pos.pos))
148 {
149 res->pos.apos[res->pos.apos[0] + 1] = LIMITPOS(ptr->pos.pos);
150 res->pos.apos[0]++;
151 }
152 }
153 }
154 ptr++;
155 }
156
157 return res + 1 - a;
158 }
159
160 /*
161 * make value of tsvector, given parsed text
162 *
163 * Note: frees prs->words and subsidiary data.
164 */
165 TSVector
make_tsvector(ParsedText * prs)166 make_tsvector(ParsedText *prs)
167 {
168 int i,
169 j,
170 lenstr = 0,
171 totallen;
172 TSVector in;
173 WordEntry *ptr;
174 char *str;
175 int stroff;
176
177 /* Merge duplicate words */
178 if (prs->curwords > 0)
179 prs->curwords = uniqueWORD(prs->words, prs->curwords);
180
181 /* Determine space needed */
182 for (i = 0; i < prs->curwords; i++)
183 {
184 lenstr += prs->words[i].len;
185 if (prs->words[i].alen)
186 {
187 lenstr = SHORTALIGN(lenstr);
188 lenstr += sizeof(uint16) + prs->words[i].pos.apos[0] * sizeof(WordEntryPos);
189 }
190 }
191
192 if (lenstr > MAXSTRPOS)
193 ereport(ERROR,
194 (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
195 errmsg("string is too long for tsvector (%d bytes, max %d bytes)", lenstr, MAXSTRPOS)));
196
197 totallen = CALCDATASIZE(prs->curwords, lenstr);
198 in = (TSVector) palloc0(totallen);
199 SET_VARSIZE(in, totallen);
200 in->size = prs->curwords;
201
202 ptr = ARRPTR(in);
203 str = STRPTR(in);
204 stroff = 0;
205 for (i = 0; i < prs->curwords; i++)
206 {
207 ptr->len = prs->words[i].len;
208 ptr->pos = stroff;
209 memcpy(str + stroff, prs->words[i].word, prs->words[i].len);
210 stroff += prs->words[i].len;
211 pfree(prs->words[i].word);
212 if (prs->words[i].alen)
213 {
214 int k = prs->words[i].pos.apos[0];
215 WordEntryPos *wptr;
216
217 if (k > 0xFFFF)
218 elog(ERROR, "positions array too long");
219
220 ptr->haspos = 1;
221 stroff = SHORTALIGN(stroff);
222 *(uint16 *) (str + stroff) = (uint16) k;
223 wptr = POSDATAPTR(in, ptr);
224 for (j = 0; j < k; j++)
225 {
226 WEP_SETWEIGHT(wptr[j], 0);
227 WEP_SETPOS(wptr[j], prs->words[i].pos.apos[j + 1]);
228 }
229 stroff += sizeof(uint16) + k * sizeof(WordEntryPos);
230 pfree(prs->words[i].pos.apos);
231 }
232 else
233 ptr->haspos = 0;
234 ptr++;
235 }
236
237 if (prs->words)
238 pfree(prs->words);
239
240 return in;
241 }
242
243 Datum
to_tsvector_byid(PG_FUNCTION_ARGS)244 to_tsvector_byid(PG_FUNCTION_ARGS)
245 {
246 Oid cfgId = PG_GETARG_OID(0);
247 text *in = PG_GETARG_TEXT_PP(1);
248 ParsedText prs;
249 TSVector out;
250
251 prs.lenwords = VARSIZE_ANY_EXHDR(in) / 6; /* just estimation of word's
252 * number */
253 if (prs.lenwords < 2)
254 prs.lenwords = 2;
255 prs.curwords = 0;
256 prs.pos = 0;
257 prs.words = (ParsedWord *) palloc(sizeof(ParsedWord) * prs.lenwords);
258
259 parsetext(cfgId, &prs, VARDATA_ANY(in), VARSIZE_ANY_EXHDR(in));
260
261 PG_FREE_IF_COPY(in, 1);
262
263 out = make_tsvector(&prs);
264
265 PG_RETURN_TSVECTOR(out);
266 }
267
268 Datum
to_tsvector(PG_FUNCTION_ARGS)269 to_tsvector(PG_FUNCTION_ARGS)
270 {
271 text *in = PG_GETARG_TEXT_PP(0);
272 Oid cfgId;
273
274 cfgId = getTSCurrentConfig(true);
275 PG_RETURN_DATUM(DirectFunctionCall2(to_tsvector_byid,
276 ObjectIdGetDatum(cfgId),
277 PointerGetDatum(in)));
278 }
279
280 /*
281 * Worker function for jsonb(_string)_to_tsvector(_byid)
282 */
283 static TSVector
jsonb_to_tsvector_worker(Oid cfgId,Jsonb * jb,uint32 flags)284 jsonb_to_tsvector_worker(Oid cfgId, Jsonb *jb, uint32 flags)
285 {
286 TSVectorBuildState state;
287 ParsedText prs;
288
289 prs.words = NULL;
290 prs.curwords = 0;
291 state.prs = &prs;
292 state.cfgId = cfgId;
293
294 iterate_jsonb_values(jb, flags, &state, add_to_tsvector);
295
296 return make_tsvector(&prs);
297 }
298
299 Datum
jsonb_string_to_tsvector_byid(PG_FUNCTION_ARGS)300 jsonb_string_to_tsvector_byid(PG_FUNCTION_ARGS)
301 {
302 Oid cfgId = PG_GETARG_OID(0);
303 Jsonb *jb = PG_GETARG_JSONB_P(1);
304 TSVector result;
305
306 result = jsonb_to_tsvector_worker(cfgId, jb, jtiString);
307 PG_FREE_IF_COPY(jb, 1);
308
309 PG_RETURN_TSVECTOR(result);
310 }
311
312 Datum
jsonb_string_to_tsvector(PG_FUNCTION_ARGS)313 jsonb_string_to_tsvector(PG_FUNCTION_ARGS)
314 {
315 Jsonb *jb = PG_GETARG_JSONB_P(0);
316 Oid cfgId;
317 TSVector result;
318
319 cfgId = getTSCurrentConfig(true);
320 result = jsonb_to_tsvector_worker(cfgId, jb, jtiString);
321 PG_FREE_IF_COPY(jb, 0);
322
323 PG_RETURN_TSVECTOR(result);
324 }
325
326 Datum
jsonb_to_tsvector_byid(PG_FUNCTION_ARGS)327 jsonb_to_tsvector_byid(PG_FUNCTION_ARGS)
328 {
329 Oid cfgId = PG_GETARG_OID(0);
330 Jsonb *jb = PG_GETARG_JSONB_P(1);
331 Jsonb *jbFlags = PG_GETARG_JSONB_P(2);
332 TSVector result;
333 uint32 flags = parse_jsonb_index_flags(jbFlags);
334
335 result = jsonb_to_tsvector_worker(cfgId, jb, flags);
336 PG_FREE_IF_COPY(jb, 1);
337 PG_FREE_IF_COPY(jbFlags, 2);
338
339 PG_RETURN_TSVECTOR(result);
340 }
341
342 Datum
jsonb_to_tsvector(PG_FUNCTION_ARGS)343 jsonb_to_tsvector(PG_FUNCTION_ARGS)
344 {
345 Jsonb *jb = PG_GETARG_JSONB_P(0);
346 Jsonb *jbFlags = PG_GETARG_JSONB_P(1);
347 Oid cfgId;
348 TSVector result;
349 uint32 flags = parse_jsonb_index_flags(jbFlags);
350
351 cfgId = getTSCurrentConfig(true);
352 result = jsonb_to_tsvector_worker(cfgId, jb, flags);
353 PG_FREE_IF_COPY(jb, 0);
354 PG_FREE_IF_COPY(jbFlags, 1);
355
356 PG_RETURN_TSVECTOR(result);
357 }
358
359 /*
360 * Worker function for json(_string)_to_tsvector(_byid)
361 */
362 static TSVector
json_to_tsvector_worker(Oid cfgId,text * json,uint32 flags)363 json_to_tsvector_worker(Oid cfgId, text *json, uint32 flags)
364 {
365 TSVectorBuildState state;
366 ParsedText prs;
367
368 prs.words = NULL;
369 prs.curwords = 0;
370 state.prs = &prs;
371 state.cfgId = cfgId;
372
373 iterate_json_values(json, flags, &state, add_to_tsvector);
374
375 return make_tsvector(&prs);
376 }
377
378 Datum
json_string_to_tsvector_byid(PG_FUNCTION_ARGS)379 json_string_to_tsvector_byid(PG_FUNCTION_ARGS)
380 {
381 Oid cfgId = PG_GETARG_OID(0);
382 text *json = PG_GETARG_TEXT_P(1);
383 TSVector result;
384
385 result = json_to_tsvector_worker(cfgId, json, jtiString);
386 PG_FREE_IF_COPY(json, 1);
387
388 PG_RETURN_TSVECTOR(result);
389 }
390
391 Datum
json_string_to_tsvector(PG_FUNCTION_ARGS)392 json_string_to_tsvector(PG_FUNCTION_ARGS)
393 {
394 text *json = PG_GETARG_TEXT_P(0);
395 Oid cfgId;
396 TSVector result;
397
398 cfgId = getTSCurrentConfig(true);
399 result = json_to_tsvector_worker(cfgId, json, jtiString);
400 PG_FREE_IF_COPY(json, 0);
401
402 PG_RETURN_TSVECTOR(result);
403 }
404
405 Datum
json_to_tsvector_byid(PG_FUNCTION_ARGS)406 json_to_tsvector_byid(PG_FUNCTION_ARGS)
407 {
408 Oid cfgId = PG_GETARG_OID(0);
409 text *json = PG_GETARG_TEXT_P(1);
410 Jsonb *jbFlags = PG_GETARG_JSONB_P(2);
411 TSVector result;
412 uint32 flags = parse_jsonb_index_flags(jbFlags);
413
414 result = json_to_tsvector_worker(cfgId, json, flags);
415 PG_FREE_IF_COPY(json, 1);
416 PG_FREE_IF_COPY(jbFlags, 2);
417
418 PG_RETURN_TSVECTOR(result);
419 }
420
421 Datum
json_to_tsvector(PG_FUNCTION_ARGS)422 json_to_tsvector(PG_FUNCTION_ARGS)
423 {
424 text *json = PG_GETARG_TEXT_P(0);
425 Jsonb *jbFlags = PG_GETARG_JSONB_P(1);
426 Oid cfgId;
427 TSVector result;
428 uint32 flags = parse_jsonb_index_flags(jbFlags);
429
430 cfgId = getTSCurrentConfig(true);
431 result = json_to_tsvector_worker(cfgId, json, flags);
432 PG_FREE_IF_COPY(json, 0);
433 PG_FREE_IF_COPY(jbFlags, 1);
434
435 PG_RETURN_TSVECTOR(result);
436 }
437
438 /*
439 * Parse lexemes in an element of a json(b) value, add to TSVectorBuildState.
440 */
441 static void
add_to_tsvector(void * _state,char * elem_value,int elem_len)442 add_to_tsvector(void *_state, char *elem_value, int elem_len)
443 {
444 TSVectorBuildState *state = (TSVectorBuildState *) _state;
445 ParsedText *prs = state->prs;
446 int32 prevwords;
447
448 if (prs->words == NULL)
449 {
450 /*
451 * First time through: initialize words array to a reasonable size.
452 * (parsetext() will realloc it bigger as needed.)
453 */
454 prs->lenwords = 16;
455 prs->words = (ParsedWord *) palloc(sizeof(ParsedWord) * prs->lenwords);
456 prs->curwords = 0;
457 prs->pos = 0;
458 }
459
460 prevwords = prs->curwords;
461
462 parsetext(state->cfgId, prs, elem_value, elem_len);
463
464 /*
465 * If we extracted any words from this JSON element, advance pos to create
466 * an artificial break between elements. This is because we don't want
467 * phrase searches to think that the last word in this element is adjacent
468 * to the first word in the next one.
469 */
470 if (prs->curwords > prevwords)
471 prs->pos += 1;
472 }
473
474
475 /*
476 * to_tsquery
477 */
478
479
480 /*
481 * This function is used for morph parsing.
482 *
483 * The value is passed to parsetext which will call the right dictionary to
484 * lexize the word. If it turns out to be a stopword, we push a QI_VALSTOP
485 * to the stack.
486 *
487 * All words belonging to the same variant are pushed as an ANDed list,
488 * and different variants are ORed together.
489 */
490 static void
pushval_morph(Datum opaque,TSQueryParserState state,char * strval,int lenval,int16 weight,bool prefix)491 pushval_morph(Datum opaque, TSQueryParserState state, char *strval, int lenval, int16 weight, bool prefix)
492 {
493 int32 count = 0;
494 ParsedText prs;
495 uint32 variant,
496 pos = 0,
497 cntvar = 0,
498 cntpos = 0,
499 cnt = 0;
500 MorphOpaque *data = (MorphOpaque *) DatumGetPointer(opaque);
501
502 prs.lenwords = 4;
503 prs.curwords = 0;
504 prs.pos = 0;
505 prs.words = (ParsedWord *) palloc(sizeof(ParsedWord) * prs.lenwords);
506
507 parsetext(data->cfg_id, &prs, strval, lenval);
508
509 if (prs.curwords > 0)
510 {
511 while (count < prs.curwords)
512 {
513 /*
514 * Were any stop words removed? If so, fill empty positions with
515 * placeholders linked by an appropriate operator.
516 */
517 if (pos > 0 && pos + 1 < prs.words[count].pos.pos)
518 {
519 while (pos + 1 < prs.words[count].pos.pos)
520 {
521 /* put placeholders for each missing stop word */
522 pushStop(state);
523 if (cntpos)
524 pushOperator(state, data->qoperator, 1);
525 cntpos++;
526 pos++;
527 }
528 }
529
530 /* save current word's position */
531 pos = prs.words[count].pos.pos;
532
533 /* Go through all variants obtained from this token */
534 cntvar = 0;
535 while (count < prs.curwords && pos == prs.words[count].pos.pos)
536 {
537 variant = prs.words[count].nvariant;
538
539 /* Push all words belonging to the same variant */
540 cnt = 0;
541 while (count < prs.curwords &&
542 pos == prs.words[count].pos.pos &&
543 variant == prs.words[count].nvariant)
544 {
545 pushValue(state,
546 prs.words[count].word,
547 prs.words[count].len,
548 weight,
549 ((prs.words[count].flags & TSL_PREFIX) || prefix));
550 pfree(prs.words[count].word);
551 if (cnt)
552 pushOperator(state, OP_AND, 0);
553 cnt++;
554 count++;
555 }
556
557 if (cntvar)
558 pushOperator(state, OP_OR, 0);
559 cntvar++;
560 }
561
562 if (cntpos)
563 {
564 /* distance may be useful */
565 pushOperator(state, data->qoperator, 1);
566 }
567
568 cntpos++;
569 }
570
571 pfree(prs.words);
572
573 }
574 else
575 pushStop(state);
576 }
577
578 Datum
to_tsquery_byid(PG_FUNCTION_ARGS)579 to_tsquery_byid(PG_FUNCTION_ARGS)
580 {
581 text *in = PG_GETARG_TEXT_PP(1);
582 TSQuery query;
583 MorphOpaque data;
584
585 data.cfg_id = PG_GETARG_OID(0);
586
587 /*
588 * Passing OP_PHRASE as a qoperator makes tsquery require matching of word
589 * positions of a complex morph exactly match the tsvector. Also, when
590 * the complex morphs are connected with OP_PHRASE operator, we connect
591 * all their words into the OP_PHRASE sequence.
592 */
593 data.qoperator = OP_PHRASE;
594
595 query = parse_tsquery(text_to_cstring(in),
596 pushval_morph,
597 PointerGetDatum(&data),
598 0);
599
600 PG_RETURN_TSQUERY(query);
601 }
602
603 Datum
to_tsquery(PG_FUNCTION_ARGS)604 to_tsquery(PG_FUNCTION_ARGS)
605 {
606 text *in = PG_GETARG_TEXT_PP(0);
607 Oid cfgId;
608
609 cfgId = getTSCurrentConfig(true);
610 PG_RETURN_DATUM(DirectFunctionCall2(to_tsquery_byid,
611 ObjectIdGetDatum(cfgId),
612 PointerGetDatum(in)));
613 }
614
615 Datum
plainto_tsquery_byid(PG_FUNCTION_ARGS)616 plainto_tsquery_byid(PG_FUNCTION_ARGS)
617 {
618 text *in = PG_GETARG_TEXT_PP(1);
619 TSQuery query;
620 MorphOpaque data;
621
622 data.cfg_id = PG_GETARG_OID(0);
623
624 /*
625 * parse_tsquery() with P_TSQ_PLAIN flag takes the whole input text as a
626 * single morph. Passing OP_PHRASE as a qoperator makes tsquery require
627 * matching of all words independently on their positions.
628 */
629 data.qoperator = OP_AND;
630
631 query = parse_tsquery(text_to_cstring(in),
632 pushval_morph,
633 PointerGetDatum(&data),
634 P_TSQ_PLAIN);
635
636 PG_RETURN_POINTER(query);
637 }
638
639 Datum
plainto_tsquery(PG_FUNCTION_ARGS)640 plainto_tsquery(PG_FUNCTION_ARGS)
641 {
642 text *in = PG_GETARG_TEXT_PP(0);
643 Oid cfgId;
644
645 cfgId = getTSCurrentConfig(true);
646 PG_RETURN_DATUM(DirectFunctionCall2(plainto_tsquery_byid,
647 ObjectIdGetDatum(cfgId),
648 PointerGetDatum(in)));
649 }
650
651
652 Datum
phraseto_tsquery_byid(PG_FUNCTION_ARGS)653 phraseto_tsquery_byid(PG_FUNCTION_ARGS)
654 {
655 text *in = PG_GETARG_TEXT_PP(1);
656 TSQuery query;
657 MorphOpaque data;
658
659 data.cfg_id = PG_GETARG_OID(0);
660
661 /*
662 * parse_tsquery() with P_TSQ_PLAIN flag takes the whole input text as a
663 * single morph. Passing OP_PHRASE as a qoperator makes tsquery require
664 * matching of word positions.
665 */
666 data.qoperator = OP_PHRASE;
667
668 query = parse_tsquery(text_to_cstring(in),
669 pushval_morph,
670 PointerGetDatum(&data),
671 P_TSQ_PLAIN);
672
673 PG_RETURN_TSQUERY(query);
674 }
675
676 Datum
phraseto_tsquery(PG_FUNCTION_ARGS)677 phraseto_tsquery(PG_FUNCTION_ARGS)
678 {
679 text *in = PG_GETARG_TEXT_PP(0);
680 Oid cfgId;
681
682 cfgId = getTSCurrentConfig(true);
683 PG_RETURN_DATUM(DirectFunctionCall2(phraseto_tsquery_byid,
684 ObjectIdGetDatum(cfgId),
685 PointerGetDatum(in)));
686 }
687
688 Datum
websearch_to_tsquery_byid(PG_FUNCTION_ARGS)689 websearch_to_tsquery_byid(PG_FUNCTION_ARGS)
690 {
691 text *in = PG_GETARG_TEXT_PP(1);
692 MorphOpaque data;
693 TSQuery query = NULL;
694
695 data.cfg_id = PG_GETARG_OID(0);
696
697 /*
698 * Passing OP_PHRASE as a qoperator makes tsquery require matching of word
699 * positions of a complex morph exactly match the tsvector. Also, when
700 * the complex morphs are given in quotes, we connect all their words into
701 * the OP_PHRASE sequence.
702 */
703 data.qoperator = OP_PHRASE;
704
705 query = parse_tsquery(text_to_cstring(in),
706 pushval_morph,
707 PointerGetDatum(&data),
708 P_TSQ_WEB);
709
710 PG_RETURN_TSQUERY(query);
711 }
712
713 Datum
websearch_to_tsquery(PG_FUNCTION_ARGS)714 websearch_to_tsquery(PG_FUNCTION_ARGS)
715 {
716 text *in = PG_GETARG_TEXT_PP(0);
717 Oid cfgId;
718
719 cfgId = getTSCurrentConfig(true);
720 PG_RETURN_DATUM(DirectFunctionCall2(websearch_to_tsquery_byid,
721 ObjectIdGetDatum(cfgId),
722 PointerGetDatum(in)));
723
724 }
725