Merge branch 'staging' of https://dev.dscrs.site/discours.io/core into feature/auth-internal
This commit is contained in:
commit
91258721c6
|
@ -29,7 +29,16 @@ jobs:
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: dokku/github-action@master
|
uses: dokku/github-action@master
|
||||||
with:
|
with:
|
||||||
branch: 'dev'
|
branch: 'main'
|
||||||
force: true
|
force: true
|
||||||
git_remote_url: 'ssh://dokku@v2.discours.io:22/core'
|
git_remote_url: 'ssh://dokku@v2.discours.io:22/core'
|
||||||
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
|
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Push to dokku for staging branch
|
||||||
|
if: github.ref == 'refs/heads/staging'
|
||||||
|
uses: dokku/github-action@master
|
||||||
|
with:
|
||||||
|
branch: 'dev'
|
||||||
|
git_remote_url: 'ssh://dokku@staging.discours.io:22/core'
|
||||||
|
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
git_push_flags: '--force'
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -128,6 +128,9 @@ dmypy.json
|
||||||
.idea
|
.idea
|
||||||
temp.*
|
temp.*
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
DEBUG.log
|
||||||
|
|
||||||
discours.key
|
discours.key
|
||||||
discours.crt
|
discours.crt
|
||||||
discours.pem
|
discours.pem
|
||||||
|
@ -162,5 +165,6 @@ views.json
|
||||||
*.crt
|
*.crt
|
||||||
*cache.json
|
*cache.json
|
||||||
.cursor
|
.cursor
|
||||||
|
.devcontainer/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
|
@ -3,6 +3,7 @@ FROM python:slim
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
curl \
|
curl \
|
||||||
|
build-essential \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
11
main.py
11
main.py
|
@ -21,7 +21,7 @@ from cache.revalidator import revalidation_manager
|
||||||
from services.exception import ExceptionHandlerMiddleware
|
from services.exception import ExceptionHandlerMiddleware
|
||||||
from services.redis import redis
|
from services.redis import redis
|
||||||
from services.schema import create_all_tables, resolvers
|
from services.schema import create_all_tables, resolvers
|
||||||
from services.search import search_service
|
from services.search import search_service, initialize_search_index
|
||||||
|
|
||||||
from utils.logger import root_logger as logger
|
from utils.logger import root_logger as logger
|
||||||
from auth.internal import InternalAuthentication
|
from auth.internal import InternalAuthentication
|
||||||
|
@ -46,6 +46,15 @@ DIST_DIR = join(os.path.dirname(__file__), "dist") # Директория дл
|
||||||
INDEX_HTML = join(os.path.dirname(__file__), "index.html")
|
INDEX_HTML = join(os.path.dirname(__file__), "index.html")
|
||||||
|
|
||||||
|
|
||||||
|
async def check_search_service():
|
||||||
|
"""Check if search service is available and log result"""
|
||||||
|
info = await search_service.info()
|
||||||
|
if info.get("status") in ["error", "unavailable"]:
|
||||||
|
print(f"[WARNING] Search service unavailable: {info.get('message', 'unknown reason')}")
|
||||||
|
else:
|
||||||
|
print(f"[INFO] Search service is available: {info}")
|
||||||
|
|
||||||
|
|
||||||
async def index_handler(request: Request):
|
async def index_handler(request: Request):
|
||||||
"""
|
"""
|
||||||
Раздача основного HTML файла
|
Раздача основного HTML файла
|
||||||
|
|
28
orm/shout.py
28
orm/shout.py
|
@ -71,6 +71,34 @@ class ShoutAuthor(Base):
|
||||||
class Shout(Base):
|
class Shout(Base):
|
||||||
"""
|
"""
|
||||||
Публикация в системе.
|
Публикация в системе.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
body (str)
|
||||||
|
slug (str)
|
||||||
|
cover (str) : "Cover image url"
|
||||||
|
cover_caption (str) : "Cover image alt caption"
|
||||||
|
lead (str)
|
||||||
|
title (str)
|
||||||
|
subtitle (str)
|
||||||
|
layout (str)
|
||||||
|
media (dict)
|
||||||
|
authors (list[Author])
|
||||||
|
topics (list[Topic])
|
||||||
|
reactions (list[Reaction])
|
||||||
|
lang (str)
|
||||||
|
version_of (int)
|
||||||
|
oid (str)
|
||||||
|
seo (str) : JSON
|
||||||
|
draft (int)
|
||||||
|
created_at (int)
|
||||||
|
updated_at (int)
|
||||||
|
published_at (int)
|
||||||
|
featured_at (int)
|
||||||
|
deleted_at (int)
|
||||||
|
created_by (int)
|
||||||
|
updated_by (int)
|
||||||
|
deleted_by (int)
|
||||||
|
community (int)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__ = "shout"
|
__tablename__ = "shout"
|
||||||
|
|
|
@ -12,6 +12,10 @@ starlette
|
||||||
gql
|
gql
|
||||||
ariadne
|
ariadne
|
||||||
granian
|
granian
|
||||||
|
|
||||||
|
# NLP and search
|
||||||
|
httpx
|
||||||
|
|
||||||
orjson
|
orjson
|
||||||
pydantic
|
pydantic
|
||||||
trafilatura
|
trafilatura
|
|
@ -8,6 +8,7 @@ from resolvers.author import ( # search_authors,
|
||||||
get_author_id,
|
get_author_id,
|
||||||
get_authors_all,
|
get_authors_all,
|
||||||
load_authors_by,
|
load_authors_by,
|
||||||
|
load_authors_search,
|
||||||
update_author,
|
update_author,
|
||||||
)
|
)
|
||||||
from resolvers.community import get_communities_all, get_community
|
from resolvers.community import get_communities_all, get_community
|
||||||
|
@ -97,6 +98,7 @@ __all__ = [
|
||||||
"get_author_follows_authors",
|
"get_author_follows_authors",
|
||||||
"get_authors_all",
|
"get_authors_all",
|
||||||
"load_authors_by",
|
"load_authors_by",
|
||||||
|
"load_authors_search",
|
||||||
"update_author",
|
"update_author",
|
||||||
# "search_authors",
|
# "search_authors",
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ from services.auth import login_required
|
||||||
from services.db import local_session
|
from services.db import local_session
|
||||||
from services.redis import redis
|
from services.redis import redis
|
||||||
from services.schema import mutation, query
|
from services.schema import mutation, query
|
||||||
|
from services.search import search_service
|
||||||
from utils.logger import root_logger as logger
|
from utils.logger import root_logger as logger
|
||||||
|
|
||||||
DEFAULT_COMMUNITIES = [1]
|
DEFAULT_COMMUNITIES = [1]
|
||||||
|
@ -358,6 +359,46 @@ async def load_authors_by(_, info, by, limit, offset):
|
||||||
return await get_authors_with_stats(limit, offset, by, current_user_id, is_admin)
|
return await get_authors_with_stats(limit, offset, by, current_user_id, is_admin)
|
||||||
|
|
||||||
|
|
||||||
|
@query.field("load_authors_search")
|
||||||
|
async def load_authors_search(_, info, text: str, limit: int = 10, offset: int = 0):
|
||||||
|
"""
|
||||||
|
Resolver for searching authors by text. Works with txt-ai search endpony.
|
||||||
|
Args:
|
||||||
|
text: Search text
|
||||||
|
limit: Maximum number of authors to return
|
||||||
|
offset: Offset for pagination
|
||||||
|
Returns:
|
||||||
|
list: List of authors matching the search criteria
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get author IDs from search engine (already sorted by relevance)
|
||||||
|
search_results = await search_service.search_authors(text, limit, offset)
|
||||||
|
|
||||||
|
if not search_results:
|
||||||
|
return []
|
||||||
|
|
||||||
|
author_ids = [result.get("id") for result in search_results if result.get("id")]
|
||||||
|
if not author_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Fetch full author objects from DB
|
||||||
|
with local_session() as session:
|
||||||
|
# Simple query to get authors by IDs - no need for stats here
|
||||||
|
authors_query = select(Author).filter(Author.id.in_(author_ids))
|
||||||
|
db_authors = session.execute(authors_query).scalars().all()
|
||||||
|
|
||||||
|
if not db_authors:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Create a dictionary for quick lookup
|
||||||
|
authors_dict = {str(author.id): author for author in db_authors}
|
||||||
|
|
||||||
|
# Keep the order from search results (maintains the relevance sorting)
|
||||||
|
ordered_authors = [authors_dict[author_id] for author_id in author_ids if author_id in authors_dict]
|
||||||
|
|
||||||
|
return ordered_authors
|
||||||
|
|
||||||
|
|
||||||
def get_author_id_from(slug="", user=None, author_id=None):
|
def get_author_id_from(slug="", user=None, author_id=None):
|
||||||
try:
|
try:
|
||||||
author_id = None
|
author_id = None
|
||||||
|
|
|
@ -4,7 +4,7 @@ type Query {
|
||||||
get_author_id(user: String!): Author
|
get_author_id(user: String!): Author
|
||||||
get_authors_all: [Author]
|
get_authors_all: [Author]
|
||||||
load_authors_by(by: AuthorsBy!, limit: Int, offset: Int): [Author]
|
load_authors_by(by: AuthorsBy!, limit: Int, offset: Int): [Author]
|
||||||
# search_authors(what: String!): [Author]
|
load_authors_search(text: String!, limit: Int, offset: Int): [Author!] # Search for authors by name or bio
|
||||||
|
|
||||||
# Auth queries
|
# Auth queries
|
||||||
logout: AuthResult!
|
logout: AuthResult!
|
||||||
|
@ -41,6 +41,7 @@ type Query {
|
||||||
get_shout(slug: String, shout_id: Int): Shout
|
get_shout(slug: String, shout_id: Int): Shout
|
||||||
load_shouts_by(options: LoadShoutsOptions): [Shout]
|
load_shouts_by(options: LoadShoutsOptions): [Shout]
|
||||||
load_shouts_search(text: String!, options: LoadShoutsOptions): [SearchResult]
|
load_shouts_search(text: String!, options: LoadShoutsOptions): [SearchResult]
|
||||||
|
get_search_results_count(text: String!): CountResult!
|
||||||
load_shouts_bookmarked(options: LoadShoutsOptions): [Shout]
|
load_shouts_bookmarked(options: LoadShoutsOptions): [Shout]
|
||||||
|
|
||||||
# rating
|
# rating
|
||||||
|
|
|
@ -214,6 +214,7 @@ type CommonResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchResult {
|
type SearchResult {
|
||||||
|
id: Int!
|
||||||
slug: String!
|
slug: String!
|
||||||
title: String!
|
title: String!
|
||||||
cover: String
|
cover: String
|
||||||
|
@ -317,3 +318,7 @@ type RolesInfo {
|
||||||
permissions: [Permission!]!
|
permissions: [Permission!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CountResult {
|
||||||
|
count: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ from sqlalchemy import (
|
||||||
inspect,
|
inspect,
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import Session, configure_mappers, declarative_base
|
from sqlalchemy.orm import Session, configure_mappers, declarative_base, joinedload
|
||||||
from sqlalchemy.sql.schema import Table
|
from sqlalchemy.sql.schema import Table
|
||||||
|
|
||||||
from settings import DB_URL
|
from settings import DB_URL
|
||||||
|
@ -298,3 +298,32 @@ def get_json_builder():
|
||||||
|
|
||||||
# Используем их в коде
|
# Используем их в коде
|
||||||
json_builder, json_array_builder, json_cast = get_json_builder()
|
json_builder, json_array_builder, json_cast = get_json_builder()
|
||||||
|
|
||||||
|
# Fetch all shouts, with authors preloaded
|
||||||
|
# This function is used for search indexing
|
||||||
|
|
||||||
|
async def fetch_all_shouts(session=None):
|
||||||
|
"""Fetch all published shouts for search indexing with authors preloaded"""
|
||||||
|
from orm.shout import Shout
|
||||||
|
|
||||||
|
close_session = False
|
||||||
|
if session is None:
|
||||||
|
session = local_session()
|
||||||
|
close_session = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch only published and non-deleted shouts with authors preloaded
|
||||||
|
query = session.query(Shout).options(
|
||||||
|
joinedload(Shout.authors)
|
||||||
|
).filter(
|
||||||
|
Shout.published_at.is_not(None),
|
||||||
|
Shout.deleted_at.is_(None)
|
||||||
|
)
|
||||||
|
shouts = query.all()
|
||||||
|
return shouts
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching shouts for search indexing: {e}")
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
if close_session:
|
||||||
|
session.close()
|
1096
services/search.py
1096
services/search.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user