mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-27 19:03:09 +02:00
Resolve #3119
This commit is contained in:
parent
772258b0b8
commit
0db54386cd
14 changed files with 147 additions and 90 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -35,6 +35,19 @@ mongodb:
|
|||
8. master ブランチに戻す
|
||||
9. enjoy
|
||||
|
||||
11.4.0 (2019/04/25)
|
||||
-------------------
|
||||
### Improvements
|
||||
* 検索でローカルの投稿のみに絞れるように
|
||||
* 検索で特定のインスタンスの投稿のみに絞れるように
|
||||
* 検索で特定のユーザーの投稿のみに絞れるように
|
||||
|
||||
### Fixes
|
||||
* 投稿が増殖する問題を修正
|
||||
* ストリームで過去の投稿が流れてくる問題を修正
|
||||
* モバイル版のユーザーページで遷移してもユーザー名が変わらない問題を修正
|
||||
* お知らせを切り替えても内容が変わらない問題を修正
|
||||
|
||||
11.3.1 (2019/04/24)
|
||||
-------------------
|
||||
### Fixes
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"format": "gulp format"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "7.0.0-rc.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.15",
|
||||
"@fortawesome/free-brands-svg-icons": "5.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.7.2",
|
||||
|
@ -35,7 +36,6 @@
|
|||
"@types/dateformat": "3.0.0",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/double-ended-queue": "2.1.0",
|
||||
"@types/elasticsearch": "5.0.32",
|
||||
"@types/file-type": "10.9.1",
|
||||
"@types/gulp": "4.0.6",
|
||||
"@types/gulp-mocha": "0.0.32",
|
||||
|
@ -113,7 +113,6 @@
|
|||
"deep-equal": "1.0.1",
|
||||
"diskusage": "1.1.0",
|
||||
"double-ended-queue": "2.1.0-0",
|
||||
"elasticsearch": "15.4.1",
|
||||
"emojilib": "2.4.0",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-plugin-vue": "5.2.2",
|
||||
|
|
31
src/client/app/common/scripts/gen-search-query.ts
Normal file
31
src/client/app/common/scripts/gen-search-query.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import parseAcct from '../../../../misc/acct/parse';
|
||||
import { host as localHost } from '../../config';
|
||||
|
||||
export async function genSearchQuery(v: any, q: string) {
|
||||
let host: string;
|
||||
let userId: string;
|
||||
if (q.split(' ').some(x => x.startsWith('@'))) {
|
||||
for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) {
|
||||
if (at.includes('.')) {
|
||||
if (at === localHost || at === '.') {
|
||||
host = null;
|
||||
} else {
|
||||
host = at;
|
||||
}
|
||||
} else {
|
||||
const user = await v.$root.api('users/show', parseAcct(at)).catch(x => null);
|
||||
if (user) {
|
||||
userId = user.id;
|
||||
} else {
|
||||
// todo: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return {
|
||||
query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '),
|
||||
host: host,
|
||||
userId: userId
|
||||
};
|
||||
}
|
|
@ -3,7 +3,7 @@ import { faHistory } from '@fortawesome/free-solid-svg-icons';
|
|||
export async function search(v: any, q: string) {
|
||||
q = q.trim();
|
||||
|
||||
if (q.startsWith('@')) {
|
||||
if (q.startsWith('@') && !q.includes(' ')) {
|
||||
v.$router.push(`/${q}`);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -60,9 +60,9 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
this.makePromise().then(x => {
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.us = x;
|
||||
} else {
|
||||
|
@ -76,9 +76,9 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
fetchMoreUsers() {
|
||||
async fetchMoreUsers() {
|
||||
this.fetchingMoreUsers = true;
|
||||
this.makePromise(this.cursor).then(x => {
|
||||
await (this.makePromise(this.cursor)).then(x => {
|
||||
this.us = this.us.concat(x.users);
|
||||
this.cursor = x.cursor;
|
||||
this.fetchingMoreUsers = false;
|
||||
|
|
|
@ -110,11 +110,11 @@ export default Vue.extend({
|
|||
this.init();
|
||||
},
|
||||
|
||||
init() {
|
||||
async init() {
|
||||
this.queue = [];
|
||||
this.notes = [];
|
||||
this.fetching = true;
|
||||
this.makePromise().then(x => {
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.notes = x;
|
||||
} else {
|
||||
|
@ -129,10 +129,10 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
fetchMore() {
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import Vue from 'vue';
|
||||
import XColumn from './deck.column.vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
|
@ -25,10 +26,10 @@ export default Vue.extend({
|
|||
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search', {
|
||||
makePromise: async cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
offset: cursor ? cursor : undefined,
|
||||
query: this.q
|
||||
...(await genSearchQuery(this, this.q))
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
|
|
|
@ -105,9 +105,9 @@ export default Vue.extend({
|
|||
this.init();
|
||||
},
|
||||
|
||||
init() {
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
this.makePromise().then(x => {
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.notes = x;
|
||||
} else {
|
||||
|
@ -122,7 +122,7 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
fetchMore() {
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
|
@ -21,10 +22,10 @@ export default Vue.extend({
|
|||
i18n: i18n('desktop/views/pages/search.vue'),
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search', {
|
||||
makePromise: async cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
offset: cursor ? cursor : undefined,
|
||||
query: this.q
|
||||
...(await genSearchQuery(this, this.q))
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
|
|
|
@ -106,9 +106,9 @@ export default Vue.extend({
|
|||
this.init();
|
||||
},
|
||||
|
||||
init() {
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
this.makePromise().then(x => {
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.notes = x;
|
||||
} else {
|
||||
|
@ -123,10 +123,10 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
fetchMore() {
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
|
@ -19,10 +20,10 @@ export default Vue.extend({
|
|||
i18n: i18n('mobile/views/pages/search.vue'),
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search', {
|
||||
makePromise: async cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
query: this.q
|
||||
...(await genSearchQuery(this, this.q))
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
|
|
|
@ -1,41 +1,30 @@
|
|||
import * as elasticsearch from 'elasticsearch';
|
||||
import * as elasticsearch from '@elastic/elasticsearch';
|
||||
import config from '../config';
|
||||
import Logger from '../services/logger';
|
||||
|
||||
const esLogger = new Logger('es');
|
||||
|
||||
const index = {
|
||||
settings: {
|
||||
analysis: {
|
||||
normalizer: {
|
||||
lowercase_normalizer: {
|
||||
type: 'custom',
|
||||
filter: ['lowercase']
|
||||
}
|
||||
},
|
||||
analyzer: {
|
||||
bigram: {
|
||||
tokenizer: 'bigram_tokenizer'
|
||||
}
|
||||
},
|
||||
tokenizer: {
|
||||
bigram_tokenizer: {
|
||||
type: 'nGram',
|
||||
min_gram: 2,
|
||||
max_gram: 2
|
||||
ngram: {
|
||||
tokenizer: 'ngram'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mappings: {
|
||||
note: {
|
||||
properties: {
|
||||
text: {
|
||||
type: 'text',
|
||||
index: true,
|
||||
analyzer: 'bigram',
|
||||
normalizer: 'lowercase_normalizer'
|
||||
}
|
||||
properties: {
|
||||
text: {
|
||||
type: 'text',
|
||||
index: true,
|
||||
analyzer: 'ngram',
|
||||
},
|
||||
userId: {
|
||||
type: 'keyword',
|
||||
index: true,
|
||||
},
|
||||
userHost: {
|
||||
type: 'keyword',
|
||||
index: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,31 +32,20 @@ const index = {
|
|||
|
||||
// Init ElasticSearch connection
|
||||
const client = config.elasticsearch ? new elasticsearch.Client({
|
||||
host: `${config.elasticsearch.host}:${config.elasticsearch.port}`
|
||||
node: `http://${config.elasticsearch.host}:${config.elasticsearch.port}`,
|
||||
pingTimeout: 30000
|
||||
}) : null;
|
||||
|
||||
if (client) {
|
||||
// Send a HEAD request
|
||||
client.ping({
|
||||
// Ping usually has a 3000ms timeout
|
||||
requestTimeout: 30000
|
||||
}, error => {
|
||||
if (error) {
|
||||
esLogger.error('elasticsearch is down!');
|
||||
} else {
|
||||
esLogger.succ('elasticsearch is available!');
|
||||
}
|
||||
});
|
||||
|
||||
client.indices.exists({
|
||||
index: 'misskey'
|
||||
index: 'misskey_note'
|
||||
}).then(exist => {
|
||||
if (exist) return;
|
||||
|
||||
client.indices.create({
|
||||
index: 'misskey',
|
||||
body: index
|
||||
});
|
||||
if (!exist.body) {
|
||||
client.indices.create({
|
||||
index: 'misskey_note',
|
||||
body: index
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ApiError } from '../../error';
|
|||
import { Notes } from '../../../../models';
|
||||
import { In } from 'typeorm';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
import { ID } from '../../../../misc/cafy-id';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -29,7 +30,17 @@ export const meta = {
|
|||
offset: {
|
||||
validator: $.optional.num.min(0),
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
host: {
|
||||
validator: $.optional.nullable.str,
|
||||
default: undefined
|
||||
},
|
||||
|
||||
userId: {
|
||||
validator: $.optional.nullable.type(ID),
|
||||
default: null
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
|
@ -54,30 +65,51 @@ export const meta = {
|
|||
export default define(meta, async (ps, me) => {
|
||||
if (es == null) throw new ApiError(meta.errors.searchingNotAvailable);
|
||||
|
||||
const response = await es.search({
|
||||
index: 'misskey',
|
||||
type: 'note',
|
||||
const userQuery = ps.userId != null ? [{
|
||||
term: {
|
||||
userId: ps.userId
|
||||
}
|
||||
}] : [];
|
||||
|
||||
const hostQuery = ps.userId == null ?
|
||||
ps.host === null ? [{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'userHost'
|
||||
}
|
||||
}
|
||||
}
|
||||
}] : ps.host !== undefined ? [{
|
||||
term: {
|
||||
userHost: ps.host
|
||||
}
|
||||
}] : []
|
||||
: [];
|
||||
|
||||
const result = await es.search({
|
||||
index: 'misskey_note',
|
||||
body: {
|
||||
size: ps.limit!,
|
||||
from: ps.offset,
|
||||
query: {
|
||||
simple_query_string: {
|
||||
fields: ['text'],
|
||||
query: ps.query,
|
||||
default_operator: 'and'
|
||||
bool: {
|
||||
must: [{
|
||||
simple_query_string: {
|
||||
fields: ['text'],
|
||||
query: ps.query.toLowerCase(),
|
||||
default_operator: 'and'
|
||||
},
|
||||
}, ...hostQuery, ...userQuery]
|
||||
}
|
||||
},
|
||||
sort: [
|
||||
{ _doc: 'desc' }
|
||||
]
|
||||
sort: [{
|
||||
_doc: 'desc'
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
if (response.hits.total === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hits = response.hits.hits.map((hit: any) => hit.id);
|
||||
const hits = result.body.hits.hits.map((hit: any) => hit._id);
|
||||
|
||||
if (hits.length === 0) return [];
|
||||
|
||||
|
|
|
@ -435,11 +435,12 @@ function index(note: Note) {
|
|||
if (note.text == null || config.elasticsearch == null) return;
|
||||
|
||||
es!.index({
|
||||
index: 'misskey',
|
||||
type: 'note',
|
||||
index: 'misskey_note',
|
||||
id: note.id.toString(),
|
||||
body: {
|
||||
text: note.text
|
||||
text: note.text.toLowerCase(),
|
||||
userId: note.userId,
|
||||
userHost: note.userHost
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue