Implements search.
This commit is contained in:
parent
705f212118
commit
3e03ffe76d
9 changed files with 228 additions and 49 deletions
|
|
@ -51,7 +51,6 @@ $logo-height: $navbar-height - 20px;
|
||||||
.search-input {
|
.search-input {
|
||||||
@extend .form-input;
|
@extend .form-input;
|
||||||
border-color: $navbar-border-color-dark;
|
border-color: $navbar-border-color-dark;
|
||||||
display: none; // TODO: Make search work and remove this.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input:focus {
|
.search-input:focus {
|
||||||
|
|
@ -292,6 +291,15 @@ $views-color: #545d70;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.thread-title {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
a > div {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-username {
|
.post-username {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import auth, email, utils, buildcss
|
||||||
|
|
||||||
import frontend/threadlist except User
|
import frontend/threadlist except User
|
||||||
import frontend/[
|
import frontend/[
|
||||||
category, postlist, error, header, post, profile, user, karaxutils
|
category, postlist, error, header, post, profile, user, karaxutils, search
|
||||||
]
|
]
|
||||||
|
|
||||||
from htmlgen import tr, th, td, span, input
|
from htmlgen import tr, th, td, span, input
|
||||||
|
|
@ -1378,6 +1378,35 @@ routes:
|
||||||
createTFD()
|
createTFD()
|
||||||
resp genPostsRSS(c), "application/atom+xml"
|
resp genPostsRSS(c), "application/atom+xml"
|
||||||
|
|
||||||
|
get "/search.json":
|
||||||
|
cond "q" in request.params
|
||||||
|
let q = @"q"
|
||||||
|
cond q.len > 0
|
||||||
|
|
||||||
|
var results: seq[SearchResult] = @[]
|
||||||
|
|
||||||
|
const queryFT = "fts.sql".slurp.sql
|
||||||
|
let data = [
|
||||||
|
q, q, $ThreadsPerPage, $0, q,
|
||||||
|
q, $ThreadsPerPage, $0, q
|
||||||
|
]
|
||||||
|
for rowFT in fastRows(db, queryFT, data):
|
||||||
|
var content = rowFT[3]
|
||||||
|
try: content = content.rstToHtml() except EParseError: discard
|
||||||
|
results.add(
|
||||||
|
SearchResult(
|
||||||
|
kind: SearchResultKind(rowFT[^1].parseInt()),
|
||||||
|
threadId: rowFT[0].parseInt(),
|
||||||
|
threadTitle: rowFT[1],
|
||||||
|
postId: rowFT[2].parseInt(),
|
||||||
|
postContent: content,
|
||||||
|
creation: rowFT[4].parseInt(),
|
||||||
|
author: selectUser(rowFT[5 .. 9]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
resp Http200, $(%results), "application/json"
|
||||||
|
|
||||||
get re"/(.*)":
|
get re"/(.*)":
|
||||||
cond request.matches[0].splitFile.ext == ""
|
cond request.matches[0].splitFile.ext == ""
|
||||||
resp karaxHtml
|
resp karaxHtml
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import strformat, times, options, json, tables, sugar, httpcore, uri
|
||||||
from dom import window, Location
|
from dom import window, Location
|
||||||
|
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import jester/patterns
|
import jester/[patterns]
|
||||||
|
|
||||||
import threadlist, postlist, header, profile, newthread, error, about
|
import threadlist, postlist, header, profile, newthread, error, about
|
||||||
import resetpassword, activateemail
|
import resetpassword, activateemail, search
|
||||||
import karaxutils
|
import karaxutils
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|
@ -16,6 +16,7 @@ type
|
||||||
about: About
|
about: About
|
||||||
resetPassword: ResetPassword
|
resetPassword: ResetPassword
|
||||||
activateEmail: ActivateEmail
|
activateEmail: ActivateEmail
|
||||||
|
search: Search
|
||||||
|
|
||||||
proc copyLocation(loc: Location): Location =
|
proc copyLocation(loc: Location): Location =
|
||||||
# TODO: It sucks that I had to do this. We need a nice way to deep copy in JS.
|
# TODO: It sucks that I had to do this. We need a nice way to deep copy in JS.
|
||||||
|
|
@ -37,7 +38,8 @@ proc newState(): State =
|
||||||
newThread: newNewThread(),
|
newThread: newNewThread(),
|
||||||
about: newAbout(),
|
about: newAbout(),
|
||||||
resetPassword: newResetPassword(),
|
resetPassword: newResetPassword(),
|
||||||
activateEmail: newActivateEmail()
|
activateEmail: newActivateEmail(),
|
||||||
|
search: newSearch()
|
||||||
)
|
)
|
||||||
|
|
||||||
var state = newState()
|
var state = newState()
|
||||||
|
|
@ -65,7 +67,8 @@ proc route(routes: openarray[Route]): VNode =
|
||||||
let prefix = if appName == "/": "" else: appName
|
let prefix = if appName == "/": "" else: appName
|
||||||
for route in routes:
|
for route in routes:
|
||||||
let pattern = (prefix & route.n).parsePattern()
|
let pattern = (prefix & route.n).parsePattern()
|
||||||
let (matched, params) = pattern.match(path)
|
var (matched, params) = pattern.match(path)
|
||||||
|
parseUrlQuery($state.url.search, params)
|
||||||
if matched:
|
if matched:
|
||||||
return route.p(params)
|
return route.p(params)
|
||||||
|
|
||||||
|
|
@ -125,6 +128,11 @@ proc render(): VNode =
|
||||||
render(state.resetPassword)
|
render(state.resetPassword)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
r("/search",
|
||||||
|
(params: Params) => (
|
||||||
|
render(state.search, params["q"], getLoggedInUser())
|
||||||
|
)
|
||||||
|
),
|
||||||
r("/404",
|
r("/404",
|
||||||
(params: Params) => render404()
|
(params: Params) => render404()
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ type
|
||||||
|
|
||||||
when defined(js):
|
when defined(js):
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import karax / [kajax]
|
import karax / [kajax, kdom]
|
||||||
|
|
||||||
import login, signup, usermenu
|
import login, signup, usermenu
|
||||||
import karaxutils
|
import karaxutils
|
||||||
|
|
||||||
from dom import setTimeout, window, document, getElementById, focus
|
from dom import
|
||||||
|
setTimeout, window, document, getElementById, focus
|
||||||
|
|
||||||
|
|
||||||
type
|
type
|
||||||
State = ref object
|
State = ref object
|
||||||
|
|
@ -74,22 +76,28 @@ when defined(js):
|
||||||
proc isLoggedIn*(): bool =
|
proc isLoggedIn*(): bool =
|
||||||
not getLoggedInUser().isNone
|
not getLoggedInUser().isNone
|
||||||
|
|
||||||
|
proc onKeyDown(e: Event, n: VNode) =
|
||||||
|
let event = cast[KeyboardEvent](e)
|
||||||
|
if event.key == "Enter":
|
||||||
|
navigateTo(makeUri("/search", ("q", $n.value), reuseSearch=false))
|
||||||
|
|
||||||
proc renderHeader*(): VNode =
|
proc renderHeader*(): VNode =
|
||||||
if state.data.isNone and state.status == Http200:
|
if state.data.isNone and state.status == Http200:
|
||||||
getStatus()
|
getStatus()
|
||||||
|
|
||||||
let user = state.data.map(x => x.user).flatten
|
let user = state.data.map(x => x.user).flatten
|
||||||
result = buildHtml(tdiv()): # TODO: Why do some buildHtml's need this?
|
result = buildHtml(tdiv()):
|
||||||
header(id="main-navbar"):
|
header(id="main-navbar"):
|
||||||
tdiv(class="navbar container grid-xl"):
|
tdiv(class="navbar container grid-xl"):
|
||||||
section(class="navbar-section"):
|
section(class="navbar-section"):
|
||||||
a(href=makeUri("/")):
|
a(href=makeUri("/")):
|
||||||
img(src="/images/logo.png", id="img-logo") # TODO: Customisation.
|
img(src="/images/logo.png", id="img-logo")
|
||||||
section(class="navbar-section"):
|
section(class="navbar-section"):
|
||||||
tdiv(class="input-group input-inline"):
|
tdiv(class="input-group input-inline"):
|
||||||
input(class="search-input input-sm",
|
input(class="search-input input-sm",
|
||||||
`type`="text", placeholder="search",
|
`type`="text", placeholder="search",
|
||||||
id="search-box")
|
id="search-box",
|
||||||
|
onKeyDown=onKeyDown)
|
||||||
if state.loading:
|
if state.loading:
|
||||||
tdiv(class="loading")
|
tdiv(class="loading")
|
||||||
elif user.isNone:
|
elif user.isNone:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import strutils, options, strformat, parseutils
|
import strutils, options, strformat, parseutils, tables
|
||||||
|
|
||||||
proc parseIntSafe*(s: string, value: var int) {.noSideEffect.} =
|
proc parseIntSafe*(s: string, value: var int) {.noSideEffect.} =
|
||||||
## parses `s` into an integer in the range `validRange`. If successful,
|
## parses `s` into an integer in the range `validRange`. If successful,
|
||||||
|
|
@ -27,7 +27,7 @@ when defined(js):
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import karax / [kdom]
|
import karax / [kdom]
|
||||||
|
|
||||||
import dom except window
|
from dom import nil
|
||||||
|
|
||||||
const appName* = "/"
|
const appName* = "/"
|
||||||
|
|
||||||
|
|
@ -53,7 +53,8 @@ when defined(js):
|
||||||
(if includeHash: $window.location.hash else: "")
|
(if includeHash: $window.location.hash else: "")
|
||||||
|
|
||||||
proc makeUri*(relative: string, params: varargs[(string, string)],
|
proc makeUri*(relative: string, params: varargs[(string, string)],
|
||||||
appName=appName, includeHash=false): string =
|
appName=appName, includeHash=false,
|
||||||
|
reuseSearch=true): string =
|
||||||
var query = ""
|
var query = ""
|
||||||
for i in 0 ..< params.len:
|
for i in 0 ..< params.len:
|
||||||
let param = params[i]
|
let param = params[i]
|
||||||
|
|
@ -61,7 +62,7 @@ when defined(js):
|
||||||
query.add(param[0] & "=" & param[1])
|
query.add(param[0] & "=" & param[1])
|
||||||
|
|
||||||
if query.len > 0:
|
if query.len > 0:
|
||||||
var search = $window.location.search
|
var search = if reuseSearch: $window.location.search else: ""
|
||||||
if search.len != 0: search.add("&")
|
if search.len != 0: search.add("&")
|
||||||
search.add(query)
|
search.add(query)
|
||||||
if search[0] != '?': search = "?" & search
|
if search[0] != '?': search = "?" & search
|
||||||
|
|
@ -74,13 +75,13 @@ when defined(js):
|
||||||
dom.pushState(dom.window.history, 0, cstring"", uri)
|
dom.pushState(dom.window.history, 0, cstring"", uri)
|
||||||
|
|
||||||
# Fire the popState event.
|
# Fire the popState event.
|
||||||
dom.window.dispatchEvent(newEvent("popstate"))
|
dom.dispatchEvent(dom.window, dom.newEvent("popstate"))
|
||||||
|
|
||||||
proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb?
|
proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb?
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
# TODO: Why does Karax have it's own Node type? That's just silly.
|
# TODO: Why does Karax have it's own Node type? That's just silly.
|
||||||
let url = cast[dom.Node](n.dom).getAttribute(cstring"href")
|
let url = n.getAttr("href")
|
||||||
|
|
||||||
navigateTo(url)
|
navigateTo(url)
|
||||||
|
|
||||||
|
|
@ -99,4 +100,22 @@ when defined(js):
|
||||||
makeUri(fmt"/profile/{username}")
|
makeUri(fmt"/profile/{username}")
|
||||||
|
|
||||||
proc renderPostUrl*(threadId, postId: int): string =
|
proc renderPostUrl*(threadId, postId: int): string =
|
||||||
makeUri(fmt"/t/{threadId}#{postId}")
|
makeUri(fmt"/t/{threadId}#{postId}")
|
||||||
|
|
||||||
|
proc parseUrlQuery*(query: string, result: var Table[string, string])
|
||||||
|
{.deprecated: "use stdlib".} =
|
||||||
|
## Based on copy from Jester. Use stdlib when
|
||||||
|
## https://github.com/nim-lang/Nim/pull/7761 is merged.
|
||||||
|
var i = 0
|
||||||
|
i = query.skip("?")
|
||||||
|
while i < query.len()-1:
|
||||||
|
var key = ""
|
||||||
|
var val = ""
|
||||||
|
i += query.parseUntil(key, '=', i)
|
||||||
|
if query[i] != '=':
|
||||||
|
raise newException(ValueError, "Expected '=' at " & $i &
|
||||||
|
" but got: " & $query[i])
|
||||||
|
inc(i) # Skip =
|
||||||
|
i += query.parseUntil(val, '&', i)
|
||||||
|
inc(i) # Skip &
|
||||||
|
result[$decodeUri(key)] = $decodeUri(val)
|
||||||
|
|
@ -10,7 +10,7 @@ type
|
||||||
thread*: Thread
|
thread*: Thread
|
||||||
history*: seq[Thread] ## If the thread was edited this will contain the
|
history*: seq[Thread] ## If the thread was edited this will contain the
|
||||||
## older versions of the thread (title/category
|
## older versions of the thread (title/category
|
||||||
## changes).
|
## changes). TODO
|
||||||
posts*: seq[Post]
|
posts*: seq[Post]
|
||||||
|
|
||||||
when defined(js):
|
when defined(js):
|
||||||
|
|
@ -225,15 +225,7 @@ when defined(js):
|
||||||
tdiv(class="post-title"):
|
tdiv(class="post-title"):
|
||||||
tdiv(class="post-username"):
|
tdiv(class="post-username"):
|
||||||
text post.author.name
|
text post.author.name
|
||||||
if post.isModerated:
|
renderUserRank(post.author)
|
||||||
italic(class="fas fa-eye-slash",
|
|
||||||
title="User is moderated")
|
|
||||||
if post.author.rank == Moderator:
|
|
||||||
italic(class="fas fa-shield-alt",
|
|
||||||
title="User is a moderator")
|
|
||||||
if post.author.rank == Admin:
|
|
||||||
italic(class="fas fa-chess-knight",
|
|
||||||
title="User is an admin")
|
|
||||||
tdiv(class="post-metadata"):
|
tdiv(class="post-metadata"):
|
||||||
if post.replyingTo.isSome():
|
if post.replyingTo.isSome():
|
||||||
let replyingTo = post.replyingTo.get()
|
let replyingTo = post.replyingTo.get()
|
||||||
|
|
|
||||||
102
src/frontend/search.nim
Normal file
102
src/frontend/search.nim
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
|
||||||
|
import user, options, httpcore, json, times
|
||||||
|
type
|
||||||
|
SearchResultKind* = enum
|
||||||
|
ThreadMatch, PostMatch
|
||||||
|
|
||||||
|
SearchResult* = object
|
||||||
|
kind*: SearchResultKind
|
||||||
|
threadId*: int
|
||||||
|
postId*: int
|
||||||
|
threadTitle*: string
|
||||||
|
postContent*: string
|
||||||
|
author*: User
|
||||||
|
creation*: int64
|
||||||
|
|
||||||
|
proc isModerated*(searchResult: SearchResult): bool =
|
||||||
|
return searchResult.author.rank <= Moderated
|
||||||
|
|
||||||
|
when defined(js):
|
||||||
|
from dom import nil
|
||||||
|
|
||||||
|
include karax/prelude
|
||||||
|
import karax / [vstyles, kajax, kdom]
|
||||||
|
|
||||||
|
import karaxutils, error, threadlist, sugar
|
||||||
|
|
||||||
|
type
|
||||||
|
Search* = ref object
|
||||||
|
list: Option[seq[SearchResult]]
|
||||||
|
loading: bool
|
||||||
|
status: HttpCode
|
||||||
|
query: string
|
||||||
|
|
||||||
|
proc newSearch*(): Search =
|
||||||
|
Search(
|
||||||
|
list: none[seq[SearchResult]](),
|
||||||
|
loading: false,
|
||||||
|
status: Http200,
|
||||||
|
query: ""
|
||||||
|
)
|
||||||
|
|
||||||
|
proc onList(httpStatus: int, response: kstring, state: Search) =
|
||||||
|
state.loading = false
|
||||||
|
state.status = httpStatus.HttpCode
|
||||||
|
if state.status != Http200: return
|
||||||
|
|
||||||
|
let parsed = parseJson($response)
|
||||||
|
let list = to(parsed, seq[SearchResult])
|
||||||
|
|
||||||
|
state.list = some(list)
|
||||||
|
|
||||||
|
proc genSearchResult(searchResult: SearchResult): VNode =
|
||||||
|
let url = renderPostUrl(searchResult.threadId, searchResult.postId)
|
||||||
|
result = buildHtml():
|
||||||
|
tdiv(class="post", id = $searchResult.postId):
|
||||||
|
tdiv(class="post-icon"):
|
||||||
|
render(searchResult.author, "post-avatar")
|
||||||
|
tdiv(class="post-main"):
|
||||||
|
tdiv(class="post-title"):
|
||||||
|
tdiv(class="thread-title"):
|
||||||
|
a(href=url):
|
||||||
|
verbatim(searchResult.threadTitle)
|
||||||
|
tdiv(class="post-username"):
|
||||||
|
text searchResult.author.name
|
||||||
|
renderUserRank(searchResult.author)
|
||||||
|
tdiv(class="post-metadata"):
|
||||||
|
# TODO: History and replying to.
|
||||||
|
let title = searchResult.creation.fromUnix().local.
|
||||||
|
format("MMM d, yyyy HH:mm")
|
||||||
|
a(href=url, title=title):
|
||||||
|
text renderActivity(searchResult.creation)
|
||||||
|
tdiv(class="post-content"):
|
||||||
|
verbatim(searchResult.postContent)
|
||||||
|
|
||||||
|
proc render*(state: Search, query: string, currentUser: Option[User]): VNode =
|
||||||
|
if state.list.isNone() or state.query != query:
|
||||||
|
state.list = none[seq[SearchResult]]()
|
||||||
|
state.status = Http200
|
||||||
|
state.query = query
|
||||||
|
|
||||||
|
if state.status != Http200:
|
||||||
|
return renderError("Couldn't retrieve search results.", state.status)
|
||||||
|
|
||||||
|
if state.list.isNone:
|
||||||
|
var params = @[("q", state.query)]
|
||||||
|
let uri = makeUri("search.json", params)
|
||||||
|
ajaxGet(uri, @[], (s: int, r: kstring) => onList(s, r, state))
|
||||||
|
|
||||||
|
return buildHtml(tdiv(class="loading loading-lg"))
|
||||||
|
|
||||||
|
let list = state.list.get()
|
||||||
|
result = buildHtml():
|
||||||
|
section(class="container grid-xl"):
|
||||||
|
tdiv(class="title"):
|
||||||
|
p(): text "Search results"
|
||||||
|
tdiv(class="searchresults"):
|
||||||
|
if list.len == 0:
|
||||||
|
renderMessage("No results found", "", "fa-exclamation")
|
||||||
|
else:
|
||||||
|
for searchResult in list:
|
||||||
|
if not searchResult.visibleTo(currentUser): continue
|
||||||
|
genSearchResult(searchResult)
|
||||||
|
|
@ -49,4 +49,22 @@ when defined(js):
|
||||||
a(class="user-mention",
|
a(class="user-mention",
|
||||||
href=makeUri("/profile/" & user.name),
|
href=makeUri("/profile/" & user.name),
|
||||||
onClick=anchorCB):
|
onClick=anchorCB):
|
||||||
text "@" & user.name
|
text "@" & user.name
|
||||||
|
|
||||||
|
proc renderUserRank*(user: User): VNode =
|
||||||
|
result = buildHtml():
|
||||||
|
case user.rank
|
||||||
|
of Spammer, Troll, Banned:
|
||||||
|
italic(class="fas fa-eye-ban",
|
||||||
|
title="User is banned")
|
||||||
|
of Rank.User, EmailUnconfirmed:
|
||||||
|
span()
|
||||||
|
of Moderated:
|
||||||
|
italic(class="fas fa-eye-slash",
|
||||||
|
title="User is moderated")
|
||||||
|
of Moderator:
|
||||||
|
italic(class="fas fa-shield-alt",
|
||||||
|
title="User is a moderator")
|
||||||
|
of Admin:
|
||||||
|
italic(class="fas fa-chess-knight",
|
||||||
|
title="User is an admin")
|
||||||
35
src/fts.sql
35
src/fts.sql
|
|
@ -4,19 +4,21 @@
|
||||||
SELECT
|
SELECT
|
||||||
thread_id,
|
thread_id,
|
||||||
snippet(thread_fts, '<b>', '</b>', '<b>...</b>') AS thread,
|
snippet(thread_fts, '<b>', '</b>', '<b>...</b>') AS thread,
|
||||||
0 AS post_id,
|
post_id,
|
||||||
'' AS header,
|
post_content,
|
||||||
'' AS content,
|
|
||||||
person.name AS author,
|
|
||||||
cdate,
|
cdate,
|
||||||
author_id,
|
person.name AS author,
|
||||||
person.email AS email,
|
person.email AS email,
|
||||||
|
strftime('%s', person.lastOnline) AS lastOnline,
|
||||||
|
person.status AS status,
|
||||||
|
person.isDeleted as person_isDeleted,
|
||||||
0 AS what
|
0 AS what
|
||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
thread_fts.id AS thread_id,
|
thread_fts.id AS thread_id,
|
||||||
post.id AS post_id,
|
post.id AS post_id,
|
||||||
post.creation AS cdate,
|
post.content AS post_content,
|
||||||
|
strftime('%s', post.creation) AS cdate,
|
||||||
MIN(post.creation) AS cdate,
|
MIN(post.creation) AS cdate,
|
||||||
post.author AS author_id
|
post.author AS author_id
|
||||||
FROM thread_fts
|
FROM thread_fts
|
||||||
|
|
@ -28,7 +30,7 @@ SELECT
|
||||||
FROM post_fts JOIN post USING(id)
|
FROM post_fts JOIN post USING(id)
|
||||||
WHERE post_fts MATCH ?
|
WHERE post_fts MATCH ?
|
||||||
)
|
)
|
||||||
LIMIT ? OFFSET (? - 1) * ?
|
LIMIT ? OFFSET ?
|
||||||
)
|
)
|
||||||
JOIN thread_fts ON thread_fts.id=thread_id
|
JOIN thread_fts ON thread_fts.id=thread_id
|
||||||
JOIN person ON person.id=author_id
|
JOIN person ON person.id=author_id
|
||||||
|
|
@ -40,30 +42,23 @@ SELECT
|
||||||
thread.name AS thread,
|
thread.name AS thread,
|
||||||
post.id AS post_id,
|
post.id AS post_id,
|
||||||
CASE what WHEN 1
|
CASE what WHEN 1
|
||||||
THEN snippet(post_fts, '<b>', '</b>', '...', what)
|
|
||||||
ELSE post_fts.header END AS header,
|
|
||||||
CASE what WHEN 2
|
|
||||||
THEN snippet(post_fts, '**', '**', '...', what, -45)
|
THEN snippet(post_fts, '**', '**', '...', what, -45)
|
||||||
ELSE SUBSTR(post_fts.content, 1, 200) END AS content,
|
ELSE SUBSTR(post_fts.content, 1, 200) END AS content,
|
||||||
person.name AS author,
|
|
||||||
cdate,
|
cdate,
|
||||||
post.author AS author_id,
|
person.name AS author,
|
||||||
person.email AS email,
|
person.email AS email,
|
||||||
|
strftime('%s', person.lastOnline) AS lastOnline,
|
||||||
|
person.status AS status,
|
||||||
|
person.isDeleted as person_isDeleted,
|
||||||
what
|
what
|
||||||
FROM post_fts JOIN (
|
FROM post_fts JOIN (
|
||||||
-- inner query, selects ids of matching posts, orders and limits them,
|
-- inner query, selects ids of matching posts, orders and limits them,
|
||||||
-- so snippets only for limited count of posts are created (in outer query)
|
-- so snippets only for limited count of posts are created (in outer query)
|
||||||
SELECT id, post.creation AS cdate, thread, 1 AS what, post.author AS author
|
SELECT id, strftime('%s', post.creation) AS cdate, thread, 1 AS what, post.author AS author
|
||||||
FROM post_fts JOIN post USING(id)
|
|
||||||
WHERE post_fts.header MATCH ?
|
|
||||||
GROUP BY post.header
|
|
||||||
HAVING SUBSTR(post.header,1,3)<>'Re:'
|
|
||||||
UNION
|
|
||||||
SELECT id, post.creation AS cdate, thread, 2 AS what, post.author AS author
|
|
||||||
FROM post_fts JOIN post USING(id)
|
FROM post_fts JOIN post USING(id)
|
||||||
WHERE post_fts.content MATCH ?
|
WHERE post_fts.content MATCH ?
|
||||||
ORDER BY what, cdate DESC
|
ORDER BY what, cdate DESC
|
||||||
LIMIT ? OFFSET (? - 1) * ?
|
LIMIT ? OFFSET ?
|
||||||
) AS post USING(id)
|
) AS post USING(id)
|
||||||
JOIN thread ON thread.id=thread
|
JOIN thread ON thread.id=thread
|
||||||
JOIN person ON person.id=author
|
JOIN person ON person.id=author
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue