feat: add full search for set/zset

This commit is contained in:
Lykin 2023-11-21 16:44:33 +08:00
parent eca640fc87
commit 6a048037b0
7 changed files with 87 additions and 121 deletions

View File

@ -762,6 +762,12 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
} }
case "set": case "set":
if !strings.HasPrefix(matchPattern, "*") {
matchPattern = "*" + matchPattern
}
if !strings.HasSuffix(matchPattern, "*") {
matchPattern = matchPattern + "*"
}
loadSetHandle := func() ([]types.SetEntryItem, bool, bool, error) { loadSetHandle := func() ([]types.SetEntryItem, bool, bool, error) {
var items []types.SetEntryItem var items []types.SetEntryItem
var cursor uint64 var cursor uint64
@ -769,11 +775,11 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
var subErr error var subErr error
var loadedKey []string var loadedKey []string
scanSize := int64(Preferences().GetScanSize()) scanSize := int64(Preferences().GetScanSize())
if param.Full { if param.Full || matchPattern != "*" {
// load all // load all
cursor, reset = 0, true cursor, reset = 0, true
for { for {
loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, matchPattern, scanSize).Result()
if subErr != nil { if subErr != nil {
return items, reset, false, subErr return items, reset, false, subErr
} }
@ -797,7 +803,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
} else { } else {
cursor, _, reset = getEntryCursor() cursor, _, reset = getEntryCursor()
} }
loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, matchPattern, scanSize).Result()
items = make([]types.SetEntryItem, len(loadedKey)) items = make([]types.SetEntryItem, len(loadedKey))
for i, val := range loadedKey { for i, val := range loadedKey {
items[i].Value = val items[i].Value = val
@ -820,17 +826,23 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
} }
case "zset": case "zset":
if !strings.HasPrefix(matchPattern, "*") {
matchPattern = "*" + matchPattern
}
if !strings.HasSuffix(matchPattern, "*") {
matchPattern = matchPattern + "*"
}
loadZSetHandle := func() ([]types.ZSetEntryItem, bool, bool, error) { loadZSetHandle := func() ([]types.ZSetEntryItem, bool, bool, error) {
var items []types.ZSetEntryItem var items []types.ZSetEntryItem
var reset bool var reset bool
var cursor uint64 var cursor uint64
scanSize := int64(Preferences().GetScanSize()) scanSize := int64(Preferences().GetScanSize())
var loadedVal []string var loadedVal []string
if param.Full { if param.Full || matchPattern != "*" {
// load all // load all
cursor, reset = 0, true cursor, reset = 0, true
for { for {
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() loadedVal, cursor, err = client.ZScan(ctx, key, cursor, matchPattern, scanSize).Result()
if err != nil { if err != nil {
return items, reset, false, err return items, reset, false, err
} }
@ -858,7 +870,7 @@ func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JS
} else { } else {
cursor, _, reset = getEntryCursor() cursor, _, reset = getEntryCursor()
} }
loadedVal, cursor, err = client.ZScan(ctx, key, cursor, param.MatchPattern, scanSize).Result() loadedVal, cursor, err = client.ZScan(ctx, key, cursor, matchPattern, scanSize).Result()
loadedLen := len(loadedVal) loadedLen := len(loadedVal)
items = make([]types.ZSetEntryItem, loadedLen/2) items = make([]types.ZSetEntryItem, loadedLen/2)
var score float64 var score float64

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { computed, reactive } from 'vue' import { computed, reactive } from 'vue'
import { isEmpty, trim } from 'lodash' import { debounce, isEmpty, trim } from 'lodash'
import { NButton, NInput } from 'naive-ui' import { NButton, NInput } from 'naive-ui'
const emit = defineEmits(['filterChanged', 'matchChanged']) const emit = defineEmits(['filterChanged', 'matchChanged'])
@ -31,9 +31,10 @@ const onFullSearch = () => {
} }
} }
const onInput = () => { const _onInput = () => {
emit('filterChanged', inputData.filter) emit('filterChanged', inputData.filter)
} }
const onInput = debounce(_onInput, 500, { leading: true, trailing: true })
const onClearFilter = () => { const onClearFilter = () => {
inputData.filter = '' inputData.filter = ''
@ -70,7 +71,7 @@ defineExpose({
{{ inputData.match }} {{ inputData.match }}
</n-tag> </n-tag>
</template> </template>
{{ $t('interface.full_search') }} {{ $t('interface.full_search_result', { pattern: inputData.match }) }}
</n-tooltip> </n-tooltip>
</template> </template>
</n-input> </n-input>

View File

@ -3,7 +3,7 @@ import { computed, h, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import ContentToolbar from './ContentToolbar.vue' import ContentToolbar from './ContentToolbar.vue'
import AddLink from '@/components/icons/AddLink.vue' import AddLink from '@/components/icons/AddLink.vue'
import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui' import { NButton, NCode, NIcon, useThemeVars } from 'naive-ui'
import { isEmpty, size } from 'lodash' import { isEmpty, size } from 'lodash'
import useDialogStore from 'stores/dialog.js' import useDialogStore from 'stores/dialog.js'
import { types, types as redisTypes } from '@/consts/support_redis_type.js' import { types, types as redisTypes } from '@/consts/support_redis_type.js'
@ -17,6 +17,7 @@ import IconButton from '@/components/common/IconButton.vue'
import Edit from '@/components/icons/Edit.vue' import Edit from '@/components/icons/Edit.vue'
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue' import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
import FormatSelector from '@/components/content_value/FormatSelector.vue' import FormatSelector from '@/components/content_value/FormatSelector.vue'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -51,7 +52,7 @@ const props = defineProps({
loading: Boolean, loading: Boolean,
}) })
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete']) const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete', 'match'])
/** /**
* *
@ -246,13 +247,13 @@ const onAddValue = (value) => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.SET) dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.SET)
} }
const filterValue = ref('')
const onFilterInput = (val) => { const onFilterInput = (val) => {
valueFilterOption.value = val valueFilterOption.value = val
} }
const clearFilter = () => { const onMatchInput = (matchVal, filterVal) => {
valueFilterOption.value = null valueFilterOption.value = filterVal
emit('match', matchVal)
} }
const onUpdateFilter = (filters, sourceColumn) => { const onUpdateFilter = (filters, sourceColumn) => {
@ -263,10 +264,11 @@ const onFormatChanged = (selDecode, selFormat) => {
emit('reload', selDecode, selFormat) emit('reload', selDecode, selFormat)
} }
const searchInputRef = ref(null)
defineExpose({ defineExpose({
reset: () => { reset: () => {
clearFilter()
resetEdit() resetEdit()
searchInputRef.value?.reset()
}, },
}) })
</script> </script>
@ -287,12 +289,10 @@ defineExpose({
@rename="emit('rename')" /> @rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input <content-search-input
v-model:value="filterValue" ref="searchInputRef"
:placeholder="$t('interface.search')" @filter-changed="onFilterInput"
clearable @match-changed="onMatchInput" />
@clear="clearFilter"
@update:value="onFilterInput" />
</div> </div>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<n-button-group> <n-button-group>

View File

@ -100,13 +100,14 @@ const loadData = async (reset, full, selMatch) => {
*/ */
const onReload = async (selDecode, selFormat) => { const onReload = async (selDecode, selFormat) => {
try { try {
const { name, db, keyCode, keyPath, decode, format } = data.value const { name, db, keyCode, keyPath, decode, format, matchPattern } = data.value
await browserStore.reloadKey({ await browserStore.reloadKey({
server: name, server: name,
db, db,
key: keyCode || keyPath, key: keyCode || keyPath,
decode: selDecode || decode, decode: selDecode || decode,
format: selFormat || format, format: selFormat || format,
matchPattern,
}) })
} finally { } finally {
} }

View File

@ -3,7 +3,7 @@ import { computed, h, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import ContentToolbar from './ContentToolbar.vue' import ContentToolbar from './ContentToolbar.vue'
import AddLink from '@/components/icons/AddLink.vue' import AddLink from '@/components/icons/AddLink.vue'
import { NButton, NCode, NIcon, NInput, useThemeVars } from 'naive-ui' import { NButton, NCode, NIcon, useThemeVars } from 'naive-ui'
import { types, types as redisTypes } from '@/consts/support_redis_type.js' import { types, types as redisTypes } from '@/consts/support_redis_type.js'
import EditableTableColumn from '@/components/common/EditableTableColumn.vue' import EditableTableColumn from '@/components/common/EditableTableColumn.vue'
import { isEmpty, size } from 'lodash' import { isEmpty, size } from 'lodash'
@ -17,6 +17,7 @@ import IconButton from '@/components/common/IconButton.vue'
import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue' import ContentEntryEditor from '@/components/content_value/ContentEntryEditor.vue'
import FormatSelector from '@/components/content_value/FormatSelector.vue' import FormatSelector from '@/components/content_value/FormatSelector.vue'
import Edit from '@/components/icons/Edit.vue' import Edit from '@/components/icons/Edit.vue'
import ContentSearchInput from '@/components/content_value/ContentSearchInput.vue'
const i18n = useI18n() const i18n = useI18n()
const themeVars = useThemeVars() const themeVars = useThemeVars()
@ -51,7 +52,7 @@ const props = defineProps({
loading: Boolean, loading: Boolean,
}) })
const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete']) const emit = defineEmits(['loadmore', 'loadall', 'reload', 'rename', 'delete', 'match'])
/** /**
* *
@ -61,18 +62,6 @@ const keyName = computed(() => {
return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath return !isEmpty(props.keyCode) ? props.keyCode : props.keyPath
}) })
const filterOption = [
{
value: 1,
label: i18n.t('common.value'),
},
{
value: 2,
label: i18n.t('common.score'),
},
]
const filterType = ref(1)
const browserStore = useBrowserStore() const browserStore = useBrowserStore()
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
const keyType = redisTypes.ZSET const keyType = redisTypes.ZSET
@ -89,46 +78,46 @@ const inEdit = computed(() => {
}) })
const fullEdit = ref(false) const fullEdit = ref(false)
const scoreFilterOption = ref(null) // const scoreFilterOption = ref(null)
const scoreColumn = computed(() => ({ const scoreColumn = computed(() => ({
key: 'score', key: 'score',
title: i18n.t('common.score'), title: i18n.t('common.score'),
align: 'center', align: 'center',
titleAlign: 'center', titleAlign: 'center',
resizable: true, resizable: true,
filterOptionValue: scoreFilterOption.value, // filterOptionValue: scoreFilterOption.value,
filter(value, row) { // filter(value, row) {
const score = parseFloat(row.s) // const score = parseFloat(row.s)
if (isNaN(score)) { // if (isNaN(score)) {
return true // return true
} // }
//
const regex = /^(>=|<=|>|<|=|!=)?(\d+(\.\d*)?)?$/ // const regex = /^(>=|<=|>|<|=|!=)?(\d+(\.\d*)?)?$/
const matches = value.match(regex) // const matches = value.match(regex)
if (matches) { // if (matches) {
const operator = matches[1] || '' // const operator = matches[1] || ''
const filterScore = parseFloat(matches[2] || '') // const filterScore = parseFloat(matches[2] || '')
if (!isNaN(filterScore)) { // if (!isNaN(filterScore)) {
switch (operator) { // switch (operator) {
case '>=': // case '>=':
return score >= filterScore // return score >= filterScore
case '<=': // case '<=':
return score <= filterScore // return score <= filterScore
case '>': // case '>':
return score > filterScore // return score > filterScore
case '<': // case '<':
return score < filterScore // return score < filterScore
case '=': // case '=':
return score === filterScore // return score === filterScore
case '!=': // case '!=':
return score !== filterScore // return score !== filterScore
} // }
} // }
} else { // } else {
return !!~row.v.indexOf(value.toString()) // return !!~row.v.indexOf(value.toString())
} // }
return true // return true
}, // },
render: (row) => { render: (row) => {
return row.s return row.s
}, },
@ -294,52 +283,28 @@ const onAddRow = () => {
dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.ZSET) dialogStore.openAddFieldsDialog(props.name, props.db, props.keyPath, props.keyCode, types.ZSET)
} }
const filterValue = ref('')
const onFilterInput = (val) => { const onFilterInput = (val) => {
switch (filterType.value) { valueFilterOption.value = val
case filterOption[0].value:
// filter value
scoreFilterOption.value = null
valueFilterOption.value = val
break
case filterOption[1].value:
// filter score
valueFilterOption.value = null
scoreFilterOption.value = val
break
}
} }
const onChangeFilterType = (type) => { const onMatchInput = (matchVal, filterVal) => {
onFilterInput(filterValue.value) valueFilterOption.value = filterVal
} emit('match', matchVal)
const clearFilter = () => {
valueFilterOption.value = null
scoreFilterOption.value = null
} }
const onUpdateFilter = (filters, sourceColumn) => { const onUpdateFilter = (filters, sourceColumn) => {
switch (filterType.value) { valueFilterOption.value = filters[sourceColumn.key]
case filterOption[0].value:
// filter value
valueFilterOption.value = filters[sourceColumn.key]
break
case filterOption[1].value:
// filter score
scoreFilterOption.value = filters[sourceColumn.key]
break
}
} }
const onFormatChanged = (selDecode, selFormat) => { const onFormatChanged = (selDecode, selFormat) => {
emit('reload', selDecode, selFormat) emit('reload', selDecode, selFormat)
} }
const searchInputRef = ref(null)
defineExpose({ defineExpose({
reset: () => { reset: () => {
clearFilter()
resetEdit() resetEdit()
searchInputRef.value?.reset()
}, },
}) })
</script> </script>
@ -360,25 +325,10 @@ defineExpose({
@rename="emit('rename')" /> @rename="emit('rename')" />
<div class="tb2 value-item-part flex-box-h"> <div class="tb2 value-item-part flex-box-h">
<div class="flex-box-h"> <div class="flex-box-h">
<n-input-group> <content-search-input
<n-select ref="searchInputRef"
v-model:value="filterType" @filter-changed="onFilterInput"
:consistent-menu-width="false" @match-changed="onMatchInput" />
:options="filterOption"
style="width: 120px"
@update:value="onChangeFilterType" />
<n-tooltip :delay="500" :disabled="filterType !== 2">
<template #trigger>
<n-input
v-model:value="filterValue"
:placeholder="$t('interface.search')"
clearable
@clear="clearFilter"
@update:value="onFilterInput" />
</template>
<div class="text-block">{{ $t('interface.score_filter_tip') }}</div>
</n-tooltip>
</n-input-group>
</div> </div>
<div class="flex-item-expand"></div> <div class="flex-item-expand"></div>
<n-button-group> <n-button-group>

View File

@ -82,6 +82,7 @@
"unpin_edit": "Cancel Pin", "unpin_edit": "Cancel Pin",
"search": "Search", "search": "Search",
"full_search": "Full Search", "full_search": "Full Search",
"full_search_result": "The content has been matched as '*{pattern}*'",
"filter_field": "Filter Field", "filter_field": "Filter Field",
"filter_value": "Filter Value", "filter_value": "Filter Value",
"length": "Length", "length": "Length",

View File

@ -81,7 +81,8 @@
"pin_edit": "固定编辑框(保存后不关闭)", "pin_edit": "固定编辑框(保存后不关闭)",
"unpin_edit": "取消固定", "unpin_edit": "取消固定",
"search": "搜索", "search": "搜索",
"full_search": "全文搜索", "full_search": "全文匹配",
"full_search_result": "内容已匹配为 *{pattern}*",
"filter_field": "筛选字段", "filter_field": "筛选字段",
"filter_value": "筛选值", "filter_value": "筛选值",
"length": "长度", "length": "长度",