mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: support multiple terms in search, remove tag support in search (#5395)
1. Removing tag support in search 2. Adding multi keyword support for search
This commit is contained in:
		
							parent
							
								
									5414fa6663
								
							
						
					
					
						commit
						432aed3034
					
				@ -84,7 +84,13 @@ export default class FeatureSearchController extends Controller {
 | 
				
			|||||||
                favoritesFirst,
 | 
					                favoritesFirst,
 | 
				
			||||||
            } = req.query;
 | 
					            } = req.query;
 | 
				
			||||||
            const userId = req.user.id;
 | 
					            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
 | 
					            const normalizedStatus = status
 | 
				
			||||||
                ?.map((tag) => tag.split(':'))
 | 
					                ?.map((tag) => tag.split(':'))
 | 
				
			||||||
                .filter(
 | 
					                .filter(
 | 
				
			||||||
@ -100,7 +106,7 @@ export default class FeatureSearchController extends Controller {
 | 
				
			|||||||
                sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
 | 
					                sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
 | 
				
			||||||
            const normalizedFavoritesFirst = favoritesFirst === 'true';
 | 
					            const normalizedFavoritesFirst = favoritesFirst === 'true';
 | 
				
			||||||
            const { features, total } = await this.featureSearchService.search({
 | 
					            const { features, total } = await this.featureSearchService.search({
 | 
				
			||||||
                query,
 | 
					                queryParams: normalizedQuery,
 | 
				
			||||||
                projectId,
 | 
					                projectId,
 | 
				
			||||||
                type,
 | 
					                type,
 | 
				
			||||||
                userId,
 | 
					                userId,
 | 
				
			||||||
 | 
				
			|||||||
@ -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 () => {
 | 
					test('should return all feature tags', async () => {
 | 
				
			||||||
    await app.createFeature('my_feature_a');
 | 
					    await app.createFeature('my_feature_a');
 | 
				
			||||||
    await app.addTag('my_feature_a', {
 | 
					    await app.addTag('my_feature_a', {
 | 
				
			||||||
@ -500,3 +456,31 @@ test('should search features by description', async () => {
 | 
				
			|||||||
        features: [{ name: 'my_feature_b', description }],
 | 
					        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' },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -529,7 +529,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
				
			|||||||
    async searchFeatures({
 | 
					    async searchFeatures({
 | 
				
			||||||
        projectId,
 | 
					        projectId,
 | 
				
			||||||
        userId,
 | 
					        userId,
 | 
				
			||||||
        query: queryString,
 | 
					        queryParams,
 | 
				
			||||||
        type,
 | 
					        type,
 | 
				
			||||||
        tag,
 | 
					        tag,
 | 
				
			||||||
        status,
 | 
					        status,
 | 
				
			||||||
@ -542,9 +542,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
				
			|||||||
        features: IFeatureOverview[];
 | 
					        features: IFeatureOverview[];
 | 
				
			||||||
        total: number;
 | 
					        total: number;
 | 
				
			||||||
    }> {
 | 
					    }> {
 | 
				
			||||||
        const normalizedFullTag = tag?.filter((tag) => tag.length === 2);
 | 
					 | 
				
			||||||
        const normalizedHalfTag = tag?.filter((tag) => tag.length === 1).flat();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const validatedSortOrder =
 | 
					        const validatedSortOrder =
 | 
				
			||||||
            sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
 | 
					            sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -554,54 +551,33 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
				
			|||||||
                if (projectId) {
 | 
					                if (projectId) {
 | 
				
			||||||
                    query.where({ project: projectId });
 | 
					                    query.where({ project: projectId });
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                const hasQueryString = Boolean(queryString?.trim());
 | 
					                const hasQueryString = queryParams?.length;
 | 
				
			||||||
                const hasHalfTag =
 | 
					
 | 
				
			||||||
                    normalizedHalfTag && normalizedHalfTag.length > 0;
 | 
					                if (hasQueryString) {
 | 
				
			||||||
                if (hasQueryString || hasHalfTag) {
 | 
					                    const sqlParameters = queryParams.map(
 | 
				
			||||||
                    const tagQuery = this.db
 | 
					                        (item) => `%${item}%`,
 | 
				
			||||||
                        .from('feature_tag')
 | 
					                    );
 | 
				
			||||||
                        .select('feature_name');
 | 
					                    const sqlQueryParameters = sqlParameters
 | 
				
			||||||
                    // todo: we can run a cheaper query when no colon is detected
 | 
					                        .map(() => '?')
 | 
				
			||||||
                    if (hasQueryString) {
 | 
					                        .join(',');
 | 
				
			||||||
                        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],
 | 
					 | 
				
			||||||
                            );
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    query.where((builder) => {
 | 
					                    query.where((builder) => {
 | 
				
			||||||
                        builder
 | 
					                        builder
 | 
				
			||||||
                            .whereILike('features.name', `%${queryString}%`)
 | 
					                            .orWhereRaw(
 | 
				
			||||||
                            .orWhereILike(
 | 
					                                `(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
 | 
				
			||||||
                                'features.description',
 | 
					                                ['features.name', ...sqlParameters],
 | 
				
			||||||
                                `%${queryString}%`,
 | 
					 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                            .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
 | 
					                    const tagQuery = this.db
 | 
				
			||||||
                        .from('feature_tag')
 | 
					                        .from('feature_tag')
 | 
				
			||||||
                        .select('feature_name')
 | 
					                        .select('feature_name')
 | 
				
			||||||
                        .whereIn(['tag_type', 'tag_value'], normalizedFullTag);
 | 
					                        .whereIn(['tag_type', 'tag_value'], tag);
 | 
				
			||||||
                    query.whereIn('features.name', tagQuery);
 | 
					                    query.whereIn('features.name', tagQuery);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                if (type) {
 | 
					                if (type) {
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,7 @@ export interface FeatureConfigurationClient {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface IFeatureSearchParams {
 | 
					export interface IFeatureSearchParams {
 | 
				
			||||||
    userId: number;
 | 
					    userId: number;
 | 
				
			||||||
    query?: string;
 | 
					    queryParams?: string[];
 | 
				
			||||||
    projectId?: string;
 | 
					    projectId?: string;
 | 
				
			||||||
    type?: string[];
 | 
					    type?: string[];
 | 
				
			||||||
    tag?: string[][];
 | 
					    tag?: string[][];
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user