전체 텍스트 검색
개시하다
Zenn의 로드맵에는 책의 전체 텍스트 검색이 있지만 현재 제목과 장과 절의 일치된 검색만 지원합니다
Zenn Roadmap · zenn-dev/zenn-community
어떻게 해야만 전문 검색을 할 수 있습니까?
원고 창고를 직접 검색하다
Zenn은 발언 데이터를 GiitHub 계정과 연결할 수 있습니다.
이에 따라 검색하고자 하는 책의 원고는 기릿허브가 창고를 공개한 경우 기릿허브에서 전문을 검색할 수 있다.
("차크라 UI의 걷기 & 팁 세트"의 창고를 검색한 모습)
그러나 여러 권의 책을 가로질러 검색할 수 없고, 지아이허브 협업이 없거나 창고가 공개되지 않았다면 이 방법을 사용할 수 없다.
이번에 나는 여러 권의 책을 횡단 검색하고 싶어서 바꿀 방법을 찾았다.
내부 API를 사용하여 본문 데이터 클론
Zenn의 로드맵에 따르면 API에서 제공하는 스레드가 있지만 일정은 없습니다.
Zenn과 Zenn이 서명한 나 사이사용 약관에 근거하여 사이트 데이터를 방문하여 이 문제에 대응한다.
역방향 프로젝트의 상세한 정보를 공개하는 것이 목적이 아니기 때문에 정보는 숨겨지지만 사이트 내부 API에서 다음과 같은 정보를 얻을 수 있다
그러나 일반적으로 호스트 전문 검색 시스템의 비용은 증가할 것이다.쉽게 세울 수 있는 방법은 없을까요?
Google Drive API를 사용한 검색 인덱스
Google Drive API는 저장된 파일의 전체 텍스트 검색 기능을 제공합니다.
Search for files and folders | Google Drive API | Google Developers
그리고 모든 파일에 속성을 추가할 수 있습니다. 이 키=value 형식의 메타데이터
Properties | Google Drive API | Google Developers
이 두 개를 조합해서.
다음은 JavaScript를 사용하는 특정 응용 프로그램 설치에 대한 설명입니다.
Service Accent 릴리즈
https://console.cloud.google.com/iam-admin/iam부터 Google Drive API 파일 로그인 및 검색을 위한 서비스 어카운트를 게시합니다.
그런 다음 https://drive.google.com/drive/my-drive에서 이 서비스 accement에서 읽고 쓸 수 있는 루트 폴더를 만듭니다.Service Accent 가 아닌 자체 Google 계정입니다.
이 폴더에는 아까 서비스 어카운트에 대한 전자 메일 편집 권한이 부여됩니다.
이 Service Accent 키를 생성하여 프로그램에서 사용합니다.
Creating and managing service account keys | Cloud IAM Documentation | Google Cloud
Google Drive에 업로드
Google APIs Node.js Client를 사용합니다.
초기화 처리
const {google} = require('googleapis');
const auth = new google.auth.GoogleAuth({
credentials: {
client_email: SERVICE_ACCOUNT_EMAIL,
private_key: `-----BEGIN PRIVATE KEY-----\n${GOOGLE_PRIVATE_KEY}\n-----END PRIVATE KEY-----`,
},
scopes: 'https://www.googleapis.com/auth/drive.file',
});
google.options({auth});
const service = google.drive('v3');
SERVICE_ACCOUNT_EMAIL 및 GOOGLEPRIVATE_KEY는 방금 만든 서비스 어카운트의 열쇠 JSON에 포함되어 있습니다.디스크에 작성된 JSON에서도 조립이 가능하지만, 운행 시 상황을 고려해 환경 변수에 숨겨진 데이터를 메모리에 넣어 처리하기 위해 이 같은 설계를 적용했다.
폴더 생성 함수
이번 시스템에서는 젠의 책 자체가 워낙 적어 영향은 없지만, 구글 드라이브의 공유 폴더는 1개에 50만 개의 폴더로 제한돼 있다.
Any single folder in Drive which is not in a shared drive can have a maximum of 500,000 items placed within it.
https://support.google.com/a/answer/2490100
따라서 이렇게 자동화되면 세척이 안전하다.다음 예에서 하위 폴더를 한 권씩 잘랐습니다.
async function createFolder(book) {
const name = book.user.username + '/' + book.slug
const exist = await service.files.list({
q: `name = '${name}' and mimeType = 'application/vnd.google-apps.folder' and '${ROOT_FOLDER_ID}' in parents`,
fields: 'files(id, name)',
})
if (exist.data.files.length > 0) {
await service.files.delete({fileId: exist.data.files[0].id})
}
const bookFolder = await service.files.create({
requestBody: {
name,
mimeType: 'application/vnd.google-apps.folder',
parents: [ROOT_FOLDER_ID],
}
})
return bookFolder.data
}
Google 드라이브 중복 폴더 이름을 허용합니다.같은 책을 여러 번 저장할 때 두 개의 폴더service.files.delete()
가 나타나지 않도록 기존 폴더를 미리 삭제한다.업로드 함수
const assert = require('assert');
async function upload(folder, book, chapter, order=0) {
assert(chapter.allowed, 'Chapter is not allowed')
assert(chapter.body_html, 'body_html is not defined')
const properties = {
'order': order,
'book': book.title.slice(0, 40),
'username': book.user.username,
'slug': book.slug,
'chapter_title': chapter.title,
'chapter_id': chapter.id,
'chapter_slug': chapter.slug,
'cover_url': book.cover_image_small_url
? book.cover_image_small_url.replace(/^.*(https:\/\/storage.googleapis.com)/, "$1")
: 'https://zenn.dev/images/book.png',
'avatar_url': book.user.avatar_small_url.indexOf('storage.googleapis.com') > -1
? book.user.avatar_small_url.replace(/^.*(https:\/\/storage.googleapis.com)/, "$1")
: ''
}
const description = chapter.body_html.replace(/(<([^>]+)>)/ig, '').slice(0, 4000)
const res = await service.files.create(
{
requestBody: {
mimeType: 'application/vnd.google-apps.document',
parents: [folder.id],
name: book.title + ' - ' + chapter.title,
description,
createdTime: chapter.body_updated_at,
properties,
},
media: {
mimeType: 'text/html',
body: chapter.body_html,
},
}
);
return res.data
}
book.title.slice(0, 40)book.cover_image_small_url.replace(/.../)
이렇게 문자 수를 자르는 것은 속성에 UTF-8124바이트 제한이 있기 때문이다.
또 하나, description에 삭제된 HTML 텍스트를 넣은 것은 검색 결과의 높은 광도를 실현하기 위한 경로입니다.여기도 글자수 제한으로 결말이 줄어들었는데...
("("Angular After Tutorial"제1장에 저장된 모습)
전체 텍스트 검색 함수
async function searchFromDrive(input, opts={}) {
const {pageToken, username, slug} = opts
let query = `mimeType = 'application/vnd.google-apps.document' and `
query += `fullText contains '${input}' and `
if (username) {
query += `properties has { key='username' and value='${username}' } and `
}
if (slug) {
query += `properties has { key='slug' and value='${slug}' } and `
}
query += 'trashed = false'
const res = await service.files.list({
q: query,
pageToken,
fields: 'files(id, name, parents, description, properties), nextPageToken',
});
const books = filesToBooks(res.data.files, input)
return {books, nextPageToken: res.data.nextPageToken};
}
흔치 않은 처리이지만 속성 구조를 이용한 검색 조회는 동적 으로 목표 파일로 조립된다.조회의 문법은 아래에 문서가 있다.Search for files and folders | Google Drive API | Google Developers
페이지Token은 이름을 붙이는 데 사용됩니다.filterToBooks()는 응용 프로그램 고유의 성형 처리이기 때문에 생략합니다.
또한 각 작가의 일람과 책의 제목이 일치하기만 한다면 속성을 물어보기만 하면 된다
async function searchBooks(input, opts={}) {
const {pageToken, username} = opts
let query = `mimeType = 'application/vnd.google-apps.document' and `
query += `name contains '${input}' and `
if (username) {
query += `properties has { key='username' and value='${username}' } and `
}
query += 'trashed = false'
const res = await service.files.list({
q: query,
pageToken,
orderBy: 'createdTime',
fields: 'files(id, name, parents, description, properties), nextPageToken',
});
const books = filesToBooks(res.data.files, input)
return {books, nextPageToken: res.data.nextPageToken};
}
앞머리 해야 돼.
지금은 노드입니다.js에서 백엔드만 개발한 상태이기 때문에 이번에는 이를 사용하는 웹 응용 인터페이스를 준비했습니다.
특별히 신경 쓰지 않기 때문에 넥스트.js+Cloud Function으로 구성되어 Firebase Hosting으로 설계되었습니다.
Cloud Function 섹션
방금 만든 검색 함수는 **firebase-js-sdk로 브라우저에서 호출할 수 있습니다.
const functions = require("firebase-functions");
const {searchBooks, searchChapters} = require("./lib/drive");
exports.search = functions.region('asia-northeast1')
.https
.onCall(async (data, context) => {
const {query, username, slug, pageToken} = data;
const {auth} = context;
functions.logger.info(`search: ${query}`, {auth});
const result = await searchChapters(query, {username, slug, pageToken});
return result;
})
exports.searchBooks = functions.region('asia-northeast1')
.https
.onCall(async (data, context) => {
const {query, username, pageToken} = data;
const {auth} = context;
functions.logger.info(`search: ${query}`, {auth});
const result = await searchBooks(query, {username, pageToken});
return result;
})
책 검색 UI
검색이 실행되면 방금 디버깅한 Function이 호출됩니다.
<form onSubmit={async (event) => {
event.preventDefault()
const search = await callableAPI('searchBooks')
const query = event.target.query.value
const result = await search({query})
setBooks(result.data.books)
}}>
<div className="flex p-2">
<span className="pr-2">🔍</span>
<input
className="pl-2"
type="text" name={`query`} placeholder="Find Books"/>
</div>
</form>
callable API()의 내용import {connectFunctionsEmulator, getFunctions, httpsCallable} from "firebase/functions";
import {initializeFirebaseApp} from "./firebase";
export async function callableAPI(name) {
const app = await initializeFirebaseApp()
const functions = getFunctions(app, 'asia-northeast1')
if (process.env.NEXT_PUBLIC_FUNCTIONS_EMULATOR_HOST) {
connectFunctionsEmulator(functions, process.env.NEXT_PUBLIC_FUNCTIONS_EMULATOR_HOST, process.env.NEXT_PUBLIC_FUNCTIONS_EMULATOR_PORT);
}
return httpsCallable(functions, name)
}
chapter.body_html
는 개발 중인 localhost가 실행 중인 Function을 호출할 수 있는 편리한 도구입니다.책의 장절 전문 검색
("promise"로 "손으로 React를 쓴 Suspense의 비동기 처리 손잡이"를 검색하는 모습)
여기서도 클라우드 펀션만 호출했을 뿐 별다른 일은 하지 않았지만, 방금 설명한 description과 일치하는 결과를 bold 글꼴 디스플레이로 바꿔 돋보이는 효과를 나타냈다.
AND 검색을 지원하기 때문에
// 雑ハイライト処理
const headRegex = new RegExp('(' + input.split(/\s/).shift() + ')', 'igm')
const index = (file.description || ``).search(headRegex)
const description = index > -1
? file.description.slice(index-10, index+90).replace(headRegex, "<strong>$1</strong>")
: (file.description || ``).slice(0, 100)
connectFunctionsEmulator()
를 통과했습니다.이상은'Zenn서를 전문으로 검색할 수 있는 사이트'입니다.이 사이트는 보기에 매우 편리한데 다른 사람도 사용할 수 있습니까?
문제점
그러나 일반적으로 이 사이트를 공개하는 상황에서 데이터의 업데이트에 대해 짧은 시간에 복제를 반복하지 않으면 낡은 정보가 참조되는 문제가 있을 수 있다(이것도 일반적인 복제 개발에 존재하는 문제점이다).
예를 들어'자유영 때 무료로 공개했다가 다음 날 비공개했다','책 자체가 삭제됐다'등의 경우에는 적절히 대응해야 한다.
사용규약에서 금지된'요금 내용을 지급하지 않고 방문하는 행위'는 현금을 이용한 2차 배포를 통해 제3자에게 제공될 위험이 있다.
제 4 조 (금지사항)
어떤 수단을 통해 본 서비스의 요금 내용을 지불하지 않은 상태에서 방문하는 행위
https://zenn.dev/terms
이런 것들은 모든 책을 대상으로 해야 하기 때문에 지연되지 않고 진행하기 어렵고,'삭제 신청 기능 추가','수동으로 다시 얻을 수 있는 버튼 추가'등의 옵션도 고려할 수 있지만 모두 문제를 빙빙 돌려서 한다.
또한 젠 내부 API를 사용했기 때문에 규격 변경에 따라 파영이 중단될 가능성이 높다.
그래서 나는 이 사이트를 개인이 사용하는 범위 내에 두기로 결정했다.
끝말
Google Drive API를 사용하면 간단한 전체 텍스트 검색 기능을 갖춘 애플리케이션을 쉽게 개발할 수 있습니다.
이번 밀폐된 환경에서는'젠의 책을 전문으로 검색하고 싶다'는 목적에 도달했으나 더는 공개적으로 사용하기 어려워 포기했다.
공식적으로 기능이 제공되거나 Zenn API가 공개될 때까지 기다리겠습니다.
Reference
이 문제에 관하여(전체 텍스트 검색), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/laiso/articles/51fd2c67e59ecb텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)