Skip to content

Commit

Permalink
feat: support multiple terms in search, remove tag support in search (#…
Browse files Browse the repository at this point in the history
…5395)

1. Removing tag support in search
2. Adding multi keyword support for search
  • Loading branch information
sjaanus committed Nov 22, 2023
1 parent 5414fa6 commit 432aed3
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 90 deletions.
10 changes: 8 additions & 2 deletions src/lib/features/feature-search/feature-search-controller.ts
Expand Up @@ -84,7 +84,13 @@ export default class FeatureSearchController extends Controller {
favoritesFirst,
} = req.query;
const userId = req.user.id;
const normalizedTag = tag?.map((tag) => tag.split(':'));
const normalizedQuery = query
?.split(',')
.map((query) => query.trim())
.filter((query) => query);
const normalizedTag = tag
?.map((tag) => tag.split(':'))
.filter((tag) => tag.length === 2);
const normalizedStatus = status
?.map((tag) => tag.split(':'))
.filter(
Expand All @@ -100,7 +106,7 @@ export default class FeatureSearchController extends Controller {
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
const normalizedFavoritesFirst = favoritesFirst === 'true';
const { features, total } = await this.featureSearchService.search({
query,
queryParams: normalizedQuery,
projectId,
type,
userId,
Expand Down
72 changes: 28 additions & 44 deletions src/lib/features/feature-search/feature.search.e2e.test.ts
Expand Up @@ -210,50 +210,6 @@ test('should filter features by environment status', async () => {
});
});

test('should filter by partial tag', async () => {
await app.createFeature('my_feature_a');
await app.createFeature('my_feature_b');
await app.addTag('my_feature_a', {
type: 'simple',
value: 'my_tag',
});

const { body } = await filterFeaturesByTag(['simple']);

expect(body).toMatchObject({
features: [{ name: 'my_feature_a' }],
});
});

test('should search matching features by tag', async () => {
await app.createFeature('my_feature_a');
await app.createFeature('my_feature_b');
await app.addTag('my_feature_a', {
type: 'simple',
value: 'my_tag',
});

const { body: fullMatch } = await searchFeatures({
query: 'simple:my_tag',
});
const { body: tagTypeMatch } = await searchFeatures({ query: 'simple' });
const { body: tagValueMatch } = await searchFeatures({ query: 'my_tag' });
const { body: partialTagMatch } = await searchFeatures({ query: 'e:m' });

expect(fullMatch).toMatchObject({
features: [{ name: 'my_feature_a' }],
});
expect(tagTypeMatch).toMatchObject({
features: [{ name: 'my_feature_a' }],
});
expect(tagValueMatch).toMatchObject({
features: [{ name: 'my_feature_a' }],
});
expect(partialTagMatch).toMatchObject({
features: [{ name: 'my_feature_a' }],
});
});

test('should return all feature tags', async () => {
await app.createFeature('my_feature_a');
await app.addTag('my_feature_a', {
Expand Down Expand Up @@ -500,3 +456,31 @@ test('should search features by description', async () => {
features: [{ name: 'my_feature_b', description }],
});
});

test('should support multiple search values', async () => {
const description = 'secretdescription';
await app.createFeature('my_feature_a');
await app.createFeature({ name: 'my_feature_b', description });
await app.createFeature('my_feature_c');

const { body } = await searchFeatures({
query: 'descr,c',
});
expect(body).toMatchObject({
features: [
{ name: 'my_feature_b', description },
{ name: 'my_feature_c' },
],
});

const { body: emptyQuery } = await searchFeatures({
query: ' , ',
});
expect(emptyQuery).toMatchObject({
features: [
{ name: 'my_feature_a' },
{ name: 'my_feature_b' },
{ name: 'my_feature_c' },
],
});
});
62 changes: 19 additions & 43 deletions src/lib/features/feature-toggle/feature-toggle-strategies-store.ts
Expand Up @@ -529,7 +529,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
async searchFeatures({
projectId,
userId,
query: queryString,
queryParams,
type,
tag,
status,
Expand All @@ -542,9 +542,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
features: IFeatureOverview[];
total: number;
}> {
const normalizedFullTag = tag?.filter((tag) => tag.length === 2);
const normalizedHalfTag = tag?.filter((tag) => tag.length === 1).flat();

const validatedSortOrder =
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';

Expand All @@ -554,54 +551,33 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
if (projectId) {
query.where({ project: projectId });
}
const hasQueryString = Boolean(queryString?.trim());
const hasHalfTag =
normalizedHalfTag && normalizedHalfTag.length > 0;
if (hasQueryString || hasHalfTag) {
const tagQuery = this.db
.from('feature_tag')
.select('feature_name');
// todo: we can run a cheaper query when no colon is detected
if (hasQueryString) {
tagQuery.whereRaw("(?? || ':' || ??) ILIKE ?", [
'tag_type',
'tag_value',
`%${queryString}%`,
]);
}
if (hasHalfTag) {
const tagParameters = normalizedHalfTag.map(
(tag) => `%${tag}%`,
);
const tagQueryParameters = normalizedHalfTag
.map(() => '?')
.join(',');
tagQuery
.orWhereRaw(
`(??) ILIKE ANY (ARRAY[${tagQueryParameters}])`,
['tag_type', ...tagParameters],
)
.orWhereRaw(
`(??) ILIKE ANY (ARRAY[${tagQueryParameters}])`,
['tag_value', ...tagParameters],
);
}
const hasQueryString = queryParams?.length;

if (hasQueryString) {
const sqlParameters = queryParams.map(
(item) => `%${item}%`,
);
const sqlQueryParameters = sqlParameters
.map(() => '?')
.join(',');

query.where((builder) => {
builder
.whereILike('features.name', `%${queryString}%`)
.orWhereILike(
'features.description',
`%${queryString}%`,
.orWhereRaw(
`(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
['features.name', ...sqlParameters],
)
.orWhereIn('features.name', tagQuery);
.orWhereRaw(
`(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
['features.description', ...sqlParameters],
);
});
}
if (normalizedFullTag && normalizedFullTag.length > 0) {
if (tag && tag.length > 0) {
const tagQuery = this.db
.from('feature_tag')
.select('feature_name')
.whereIn(['tag_type', 'tag_value'], normalizedFullTag);
.whereIn(['tag_type', 'tag_value'], tag);
query.whereIn('features.name', tagQuery);
}
if (type) {
Expand Down
Expand Up @@ -23,7 +23,7 @@ export interface FeatureConfigurationClient {

export interface IFeatureSearchParams {
userId: number;
query?: string;
queryParams?: string[];
projectId?: string;
type?: string[];
tag?: string[][];
Expand Down

0 comments on commit 432aed3

Please sign in to comment.