feat: use flexsearch for all local searches #997

Merged
konrad merged 15 commits from feature/flexsearch into main 2021-11-14 20:49:53 +00:00
Owner

This PR introduces flexsearch when searching for labels, lists and namespaces locally.

To discuss:

There's quite a bit of duplication I created here, not sure how to handle this in a better way.

Also I noticed that, while pretty similar, all three (labels, namespaces, lists) handle storing items in vuex slightly different. Labels are stored in a labels object in the state with the id as key, lists are stored directly in the state without any object in the middle of it and namespaces are stored in a namespaces array.

That has grown historically, but I think it should be refactored at some point.

This PR introduces [flexsearch](https://github.com/nextapps-de/flexsearch) when searching for labels, lists and namespaces locally. #### To discuss: There's quite a bit of duplication I created here, not sure how to handle this in a better way. Also I noticed that, while pretty similar, _all three_ (labels, namespaces, lists) handle storing items in vuex slightly different. Labels are stored in a `labels` object in the state with the id as key, lists are stored directly in the state without any object in the middle of it and namespaces are stored in a `namespaces` array. That has grown historically, but I think it should be refactored at some point.
konrad added 3 commits 2021-11-13 22:55:34 +00:00
Author
Owner

@dpschen Do you maybe have an idea how to handle this in a good way?

@dpschen Do you maybe have an idea how to handle this in a good way?
dpschen requested changes 2021-11-14 15:55:21 +00:00
dpschen left a comment
Member

In general:

  • Flexsearch seems like a nice lib!
  • ID as key is usually the way to go. With repetition of the id inside the object. I guess we should change that. The question is when :D

What I don't like:

  • flexsearch builds another index basically a repetition of some of the info in the store. But I guess there is nothing we can do about that.
  • we add new complexity that makes it harder to improve and simplify the general architecture.
In general: - Flexsearch seems like a nice lib! - ID as key is usually the way to go. With repetition of the id inside the object. I guess we should change that. The question is when :D What I don't like: - flexsearch builds another index basically a repetition of some of the info in the store. But I guess there is nothing we can do about that. - we add new complexity that makes it harder to improve and simplify the general architecture.
@ -29,0 +31,4 @@
})
return resultIds
.filter((value, index, self) => self.indexOf(value) === index && !labelIdsToHide.includes(value))

Use includes()

Use `includes()`
Author
Owner

How? I'm using this to filter out duplicate Ids.

How? I'm using this to filter out duplicate Ids.

Ignore. Sry, I misread

Ignore. Sry, I misread
dpschen marked this conversation as resolved
@ -0,0 +1,19 @@
import {createNewIndex, withId} from '@/indexes/index'

In these three files the duplication can be avoided by creating a factory function.
Something like:

// createNewIndexer.js
import {createNewIndex, withId} from '@/indexes/index'


export function createNewIndexer() {
    function add(item: withId) {
        return index.add(label.id, label)
    }

    function remove(item: withId) {
        return index.remove(label.id)
    }

    function update(item: withId) {
        return index.update(label.id, label)
    }

    function search(item: withId) {
        return index.search(query)
    }
    
    return {
    	add,
        remove,
        update,
        search
    }
}

and then:

// labels.js
import { createNewIndexer } from './createNewIndxer'

export createNewIndexer()

In these three files the duplication can be avoided by creating a factory function. Something like: ```js // createNewIndexer.js import {createNewIndex, withId} from '@/indexes/index' export function createNewIndexer() { function add(item: withId) { return index.add(label.id, label) } function remove(item: withId) { return index.remove(label.id) } function update(item: withId) { return index.update(label.id, label) } function search(item: withId) { return index.search(query) } return { add, remove, update, search } } ``` and then: ```js // labels.js import { createNewIndexer } from './createNewIndxer' export createNewIndexer() ```

On another thought:
The createNewIndexer should probably just be defined inside the index file already

On another thought: The `createNewIndexer` should probably just be defined inside the index file already
Author
Owner

That sounds like a really good idea. I've moved it all to a factory function.

That sounds like a really good idea. I've moved it all to a factory function.
konrad marked this conversation as resolved
@ -3,6 +3,7 @@ import {setLoading} from '@/store/helper'
import {success} from '@/message'
import {i18n} from '@/i18n'
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
import {add, remove, update} from '../../indexes/labels'

use @

use `@`
Author
Owner

Done.

Done.
konrad marked this conversation as resolved
@ -35,2 +39,4 @@
return typeof list === 'undefined' ? null : list
},
searchList: state => (query, includeArchived = false) => {
if (query === '' || query === null) {

Don' repeat all the search validation. Instead put that in the search function.
(you repeated this validatin multiple times)

Don' repeat all the search validation. Instead put that in the search function. (you repeated this validatin multiple times)
Author
Owner

In the indexer module? The problem here is it does not just return an array, but another object with the results in them (coming from flexsearch). Should I recreate that?

In the indexer module? The problem here is it does not just return an array, but another object with the results in them (coming from flexsearch). Should I recreate that?

I meant putting the query check in the search function that you create =)

I meant putting the query check in the search function that you create =)
Author
Owner
Just to clarify, here? https://kolaente.dev/vikunja/frontend/src/branch/feature/flexsearch/src/indexes/index.ts#L39

Yes!

Yes!
Author
Owner

Okay, but what should I return if the query is empty? I can't really return an empty array because the caller expects the object with the results from flexsearch.

Or should I return null and check that in the caller?

Okay, but what should I return if the query is empty? I can't really return an empty array because the caller expects the object with the results from flexsearch. Or should I return `null` and check that in the caller?

Return null (I would have used undefined, but after thinking about it null seems to make more sense…?).

After the call of search() you usually iterate over the results. You can use optional chaining to check is you can do that (result?.map( [...] ) || FALLBACK)

Return `null` (I would have used `undefined`, but after thinking about it `null` seems to make more sense…?). After the call of `search()` you usually iterate over the results. You can use optional chaining to check is you can do that (`result?.map( [...] ) || FALLBACK`)

Is that what you mean?

Is that what you mean?
Author
Owner

yeah pretty much.

yeah pretty much.
Author
Owner

Moved the check and simplified accessing it.

Moved the check and simplified accessing it.
konrad marked this conversation as resolved
@ -37,0 +51,4 @@
})
return resultIds
.filter((value, index, self) => self.indexOf(value) === index && value > 0)

use includes()

use `includes()`
konrad marked this conversation as resolved
@ -100,0 +112,4 @@
const namespacesFromIndex = search(query)
const resultIds = []
namespacesFromIndex.forEach(r => {

Use .map()

Use `.map()`
Author
Owner

Done.

Done.
konrad marked this conversation as resolved
@ -100,0 +117,4 @@
})
return resultIds
.filter((value, index, self) => self.indexOf(value) === index && value > 0)

Use .includes()

Use `.includes()`
konrad marked this conversation as resolved
@ -100,0 +119,4 @@
return resultIds
.filter((value, index, self) => self.indexOf(value) === index && value > 0)
.map(id => {
const namespaceIndex = state.namespaces.findIndex(n => n.id === id)

Use .find()

Use `.find()`
Author
Owner

Done.

Done.
konrad marked this conversation as resolved
konrad added 1 commit 2021-11-14 16:19:06 +00:00
chore: use @
Some checks failed
continuous-integration/drone/pr Build is failing
b0388407c2
konrad added 1 commit 2021-11-14 16:22:05 +00:00
chore: simplify searching for a namespace
Some checks failed
continuous-integration/drone/pr Build is failing
b9ccbb16e4
konrad added 1 commit 2021-11-14 16:34:12 +00:00
chore: use factory function to avoid duplication
Some checks failed
continuous-integration/drone/pr Build is failing
6d3eb97d3c
konrad added 1 commit 2021-11-14 16:38:16 +00:00
chore: simplify getting result ids
Some checks failed
continuous-integration/drone/pr Build is failing
77b91e5181
konrad added 1 commit 2021-11-14 16:54:18 +00:00
chore: move creating a new index
Some checks failed
continuous-integration/drone/pr Build is failing
2f9701f405
konrad added 1 commit 2021-11-14 18:10:45 +00:00
chore: move empty query to search method
Some checks failed
continuous-integration/drone/pr Build is failing
b6113cfba2
konrad added 1 commit 2021-11-14 18:13:43 +00:00
chore: simplify checking for empty search results
Some checks failed
continuous-integration/drone/pr Build is failing
9776b58efc
konrad added 1 commit 2021-11-14 18:15:23 +00:00
Merge branch 'main' into feature/flexsearch
Some checks failed
continuous-integration/drone/pr Build is failing
6dcefcecd1
# Conflicts:
#	package.json
konrad added 1 commit 2021-11-14 18:36:25 +00:00
fix: label tests
All checks were successful
continuous-integration/drone/pr Build is passing
ad6b3222d0
Member

Hi konrad!

Thank you for creating a PR!

I've deployed the changes of this PR on a preview environment under this URL: https://997-featureflexsearch--vikunja-frontend-preview.netlify.app

You can use this url to view the changes live and test them out.
You will need to manually connect this to an api running somehwere. The easiest to use is https://try.vikunja.io/.

Have a nice day!

Beep boop, I'm a bot.

Hi konrad! Thank you for creating a PR! I've deployed the changes of this PR on a preview environment under this URL: https://997-featureflexsearch--vikunja-frontend-preview.netlify.app You can use this url to view the changes live and test them out. You will need to manually connect this to an api running somehwere. The easiest to use is https://try.vikunja.io/. Have a nice day! > Beep boop, I'm a bot.
dpschen requested changes 2021-11-14 19:06:01 +00:00
@ -27,2 +26,2 @@
return !labelIds.includes(id) && title.toLowerCase().includes(labelQuery)
})
return search(query)
?.map(r => r.result)

Use flatMap.

Use flatMap.
Author
Owner

Oh, I didn't know that exists. Done.

Oh, I didn't know that exists. Done.
konrad marked this conversation as resolved
@ -5,1 +5,4 @@
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
import {createNewIndexer} from '@/indexes'
const {add, remove, update} = createNewIndexer('labels', ['title', 'description'])

I like that =)

I like that =)
konrad marked this conversation as resolved
@ -47,3 +55,4 @@
removeNamespaceById(state, namespaceId) {
for (const n in state.namespaces) {
if (state.namespaces[n].id === namespaceId) {
remove(state.namespaces[n])

Picky: remove after splice (if why ever splice fails remove will correctly not be reached

Picky: `remove` after splice (if why ever splice fails `remove` will correctly not be reached
Author
Owner

Done.

Done.
konrad marked this conversation as resolved
@ -100,0 +112,4 @@
.flat()
.filter((value, index, self) => self.indexOf(value) === index && value > 0)
.map(id => {
const namespace = state.namespaces.find(n => n.id === id)

Acutally: use getNamespaceById getter

Acutally: use getNamespaceById getter
Author
Owner

Done.

Done.
konrad marked this conversation as resolved
konrad added 1 commit 2021-11-14 19:26:10 +00:00
chore: simplify searching
Some checks reported errors
continuous-integration/drone/pr Build was killed
9d68566f90
konrad added 1 commit 2021-11-14 19:26:58 +00:00
chore: call remove after splice
Some checks reported errors
continuous-integration/drone/pr Build was killed
4e90319165
konrad added 1 commit 2021-11-14 19:29:24 +00:00
chore: use getter
All checks were successful
continuous-integration/drone/pr Build is passing
8afe4a9d15
dpschen approved these changes 2021-11-14 20:38:44 +00:00
konrad merged commit 507a73e74c into main 2021-11-14 20:49:53 +00:00
konrad deleted branch feature/flexsearch 2021-11-14 20:49:53 +00:00
This repo is archived. You cannot comment on pull requests.
No description provided.