Skip to content

Commit 086c4be

Browse files
authored
Merge pull request #45 from Revyy/questions-full-text-search
feat: full text search on questions page
2 parents c8db5e3 + f9a289f commit 086c4be

File tree

8 files changed

+91
-4
lines changed

8 files changed

+91
-4
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @param {import('knex').Knex} knex
3+
*/
4+
exports.up = async function up(knex) {
5+
if (knex.client.config.client === 'pg') {
6+
await knex.schema.raw(
7+
`CREATE INDEX questions_search_content_index ON questions USING GIN (to_tsvector('english', title || ' ' || content))`,
8+
);
9+
}
10+
};
11+
12+
/**
13+
* @param {import('knex').Knex} knex
14+
*/
15+
exports.down = async function down(knex) {
16+
if (knex.client.config.client === 'pg') {
17+
await knex.schema.raw(`DROP INDEX questions_search_content_index`);
18+
}
19+
};

plugins/qeta-backend/src/database/DatabaseQetaStore.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,24 @@ describe.each(databases.eachSupportedId())(
147147
expect(ret?.questions.length).toEqual(2);
148148
});
149149

150+
it('should fetch list of questions based on searchQuery', async () => {
151+
await insertQuestion(question);
152+
await insertQuestion({
153+
...question,
154+
title: 'title2',
155+
content: 'content to search for',
156+
});
157+
const ret = await storage.getQuestions('user1', {
158+
searchQuery: 'to search',
159+
});
160+
expect(ret?.questions.length).toEqual(1);
161+
162+
const noQuestions = await storage.getQuestions('user1', {
163+
searchQuery: 'missing',
164+
});
165+
expect(noQuestions?.questions.length).toEqual(0);
166+
});
167+
150168
it('should fetch questions with specific component', async () => {
151169
const q1 = await storage.postQuestion(
152170
'user1',

plugins/qeta-backend/src/database/DatabaseQetaStore.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ export class DatabaseQetaStore implements QetaStore {
9999
query.where('questions.author', '=', options.author);
100100
}
101101

102+
if (options.searchQuery) {
103+
if (this.db.client.config.client === 'pg') {
104+
query.whereRaw(
105+
`to_tsvector('english', questions.title || ' ' || questions.content) @@ websearch_to_tsquery('english', ?)`,
106+
[`${options.searchQuery}`],
107+
);
108+
} else {
109+
query.whereRaw(
110+
`LOWER(questions.title || ' ' || questions.content) LIKE LOWER(?)`,
111+
[`%${options.searchQuery}%`],
112+
);
113+
}
114+
}
115+
102116
if (options.tags) {
103117
query.leftJoin(
104118
'question_tags',

plugins/qeta-backend/src/database/QetaStore.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface QuestionsOptions {
7373

7474
includeTrend?: boolean;
7575
random?: boolean;
76+
searchQuery?: string;
7677
}
7778

7879
export interface TagResponse {

plugins/qeta-backend/src/service/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ interface QuestionsQuery {
5555
includeVotes?: boolean;
5656
includeEntities?: boolean;
5757
includeTrend?: boolean;
58+
searchQuery?: string;
5859
}
5960

6061
const QuestionsQuerySchema: JSONSchemaType<QuestionsQuery> = {
@@ -79,6 +80,7 @@ const QuestionsQuerySchema: JSONSchemaType<QuestionsQuery> = {
7980
includeVotes: { type: 'boolean', nullable: true },
8081
includeEntities: { type: 'boolean', nullable: true },
8182
includeTrend: { type: 'boolean', nullable: true },
83+
searchQuery: { type: 'string', nullable: true },
8284
},
8385
required: [],
8486
additionalProperties: false,

plugins/qeta/src/api/QetaApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type GetQuestionsOptions = {
1515
entity?: string;
1616
author?: string;
1717
favorite?: boolean;
18+
searchQuery?: string;
1819

1920
includeEntities?: boolean;
2021
};

plugins/qeta/src/components/QuestionsContainer/QuestionsContainer.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { useQetaApi } from '../../utils/hooks';
2-
import { Box, Collapse, Grid, Typography, Button } from '@material-ui/core';
2+
import {
3+
Box,
4+
Collapse,
5+
Grid,
6+
Typography,
7+
Button,
8+
TextField,
9+
} from '@material-ui/core';
10+
311
import React, { useEffect } from 'react';
12+
import useDebounce from 'react-use/lib/useDebounce';
413
import { FilterKey, filterKeys, FilterPanel } from './FilterPanel';
514
import { QuestionList } from './QuestionList';
615
import FilterList from '@material-ui/icons/FilterList';
@@ -23,12 +32,14 @@ export const QuestionsContainer = (props: QuestionsContainerProps) => {
2332
const [questionsPerPage, setQuestionsPerPage] = React.useState(10);
2433
const [showFilterPanel, setShowFilterPanel] = React.useState(false);
2534
const [searchParams, setSearchParams] = useSearchParams();
35+
const [searchQuery, setSearchQuery] = React.useState('');
2636
const [filters, setFilters] = React.useState({
2737
order: 'desc',
2838
orderBy: 'created',
2939
noAnswers: 'false',
3040
noCorrectAnswer: 'false',
3141
noVotes: 'false',
42+
searchQuery: '',
3243
});
3344

3445
const onPageChange = (value: number) => {
@@ -50,6 +61,14 @@ export const QuestionsContainer = (props: QuestionsContainerProps) => {
5061
});
5162
};
5263

64+
const onSearchQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
65+
setSearchQuery(event.target.value);
66+
};
67+
68+
useDebounce(() => setFilters({ ...filters, searchQuery: searchQuery }), 400, [
69+
searchQuery,
70+
]);
71+
5372
useEffect(() => {
5473
let filtersApplied = false;
5574
searchParams.forEach((value, key) => {
@@ -127,6 +146,20 @@ export const QuestionsContainer = (props: QuestionsContainerProps) => {
127146
return (
128147
<Box>
129148
{showTitle && <Typography variant="h5">{shownTitle}</Typography>}
149+
<Grid container>
150+
<Grid item xs={12} md={4}>
151+
<TextField
152+
id="search-bar"
153+
fullWidth
154+
onChange={onSearchQueryChange}
155+
label="Search for questions"
156+
variant="outlined"
157+
placeholder="Search..."
158+
size="small"
159+
style={{ marginBottom: '5px' }}
160+
/>
161+
</Grid>
162+
</Grid>
130163
<Grid container justifyContent="space-between">
131164
<Grid item>
132165
<Typography variant="h6">{`${

plugins/qeta/src/utils/hooks.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
IdentityApi,
55
identityApiRef,
66
useApi,
7-
configApiRef
7+
configApiRef,
88
} from '@backstage/core-plugin-api';
99
import { makeStyles } from '@material-ui/core';
1010
import { CatalogApi } from '@backstage/catalog-client';
@@ -183,7 +183,6 @@ export const useStyles = makeStyles(theme => {
183183
};
184184
});
185185

186-
187186
// Url resolving logic from https://github.com/backstage/backstage/blob/master/packages/core-components/src/components/Link/Link.tsx
188187

189188
/**
@@ -205,4 +204,4 @@ export const useBasePath = () => {
205204
const url = useBaseUrl() ?? '/';
206205
const { pathname } = new URL(url, base);
207206
return trimEnd(pathname, '/');
208-
};
207+
};

0 commit comments

Comments
 (0)