查看一些文档金格插件WebOffice2015、chrome浏览器插件、only-office、UEditor、TinyMCE、CKEditor、wangeditor、canvas-editor
最后选择了only-office和canvas-editor
only-office非常功能强大,word、ppt、excel都支持在线编辑预览,还支持协同,又有免费开源版。
附上本地运行demo:
docker run -i -t -d -p 8701:80 onlyoffice/documentserver:版本号如果是第 1 次执行这个命令,会先去下载 Onlyoffice,比较慢,约等待 3~10 分钟,网络畅通一点的会快一些。如果是已经安装过则直接进行启动。
先执行 docker ps 查看 Onlyoffice 容器 ID:
# 注意这里要将 id 替换成自己的
docker exec -it f2a3eb675ad1 /bin/bash然后执行 docker exec -it ID /bin/bash 进入容器,这里将获取到的 ID 替换一下:
# 启动所有的内置服务
supervisorctl restart all
# 退出容器
exit最后访问 http://IP:8701/example 页面(这里要注意,IP 不能是 localhost 和 127.0.0.1,一定要用真实 IP 来访问)
因为开发周期,后端又比较懒不想花时间去看文档。这一方案被我放弃了
最后选择了canvas-editor
为什么选它了,开发周期短,界面与word编辑器比较像,可以快速集成到vue,虽然作者没有开箱即用版。
在vue中主要实现方式就是采用开源项目代码。
在组件模块,新建vue文件,html采用开源项目代码,分3个部分,工具栏,侧边菜单,主要内容,底部工具,旁边批注。通过import引入开源样式,注意样式冲突。在onMounted,采用开源main.ts window.onload代码。
<div class="menu" editor-component="menu">
...
</div>
<div class="catalog" editor-component="catalog">
...
</div>
<div id="canvasEditor" class="canvas-editor"></div>
<div class="footer-canvas" editor-component="footer">
...
</div>const instance=new Editor(
document.querySelector('#canvasEditor'),
{
header: props.header,
main: props.main,
footer: props.footer
},
options
);
console.log('实例', instance);
editorRef.value=instance;
// 工具栏方法 例:
// 2. | 撤销 | 重做 | 格式刷 | 清除格式 |
const undoDom=document.querySelector('.menu-item__undo');
undoDom.title=`撤销(${isApple ? '?' : 'Ctrl'}+Z)`;
undoDom.onclick=function () {
console.log('undo');
instance.command.executeUndo();
};
<style lang="scss" scoped>
#canvasEditor {
display: flex;
justify-content: center;
background: #f2f4f7;
}
@import url(@/assets/css/dialog.css);
@import url(@/assets/css/signature.css);
</style>思路通过富文本编辑器实现在线编辑功能,通过插件提供的api获取图片base64和文本数据,通过接口保存至数据库,通过列表数据复显编辑,后端获取到图片数据转pdf或者其他格式都可以。自己转pdf则是通过html2canvas jspdf
例:
<canvas-editor
v-if="!loadingInit"
:header="canvasEditor.header"
:main="canvasEditor.main"
:footer="canvasEditor.footer"
:options="canvasEditor.options"
@getCanvasEditorData="getCanvasEditorData"
/>
import CanvasEditor from '@/components/editor/index.vue';
components: {
CanvasEditor
},
canvasEditor: {
main: [],
options: [],
header: [],
footer: []
},
// getCanvasEditorData 事件
主要获取富文本 main内容 image的base64 options配置项 用于数据保存
getCanvasEditorData: ({ value, imgage, options })=> {
},
// 页眉配置
header: {
type: Array,
default: []
},
// 主要编辑内容
main: {
type: Array,
default: []
},
// 页脚信息
footer: {
type: Array,
default: []
},
//
options: {
type: Object,
default: {}
},
// 批注 TODO
commentList: {
type: Array,
default: []
}interface IEditorOption {
mode?: EditorMode // 编辑器模式:编辑、清洁(不显示视觉辅助元素。如:分页符)、只读、表单(仅控件内可编辑)、打印(不显示辅助元素、未书写控件及前后括号)。默认:编辑
defaultType?: string // 默认元素类型。默认:TEXT
defaultColor?: string // 默认字体颜色。默认:#000000
defaultFont?: string // 默认字体。默认:Microsoft YaHei
defaultSize?: number // 默认字号。默认:16
minSize?: number // 最小字号。默认:5
maxSize?: number // 最大字号。默认:72
defaultBasicRowMarginHeight?: number // 默认行高。默认:8
defaultRowMargin?: number // 默认行间距。默认:1
defaultTabWidth?: number // 默认tab宽度。默认:32
width?: number // 纸张宽度。默认:794
height?: number // 纸张高度。默认:1123
scale?: number // 缩放比例。默认:1
pageGap?: number // 纸张间隔。默认:20
underlineColor?: string // 下划线颜色。默认:#000000
strikeoutColor?: string // 删除线颜色。默认:#FF0000
rangeColor?: string // 选区颜色。默认:#AECBFA
rangeAlpha?: number // 选区透明度。默认:0.6
rangeMinWidth?: number // 选区最小宽度。默认:5
searchMatchColor?: string // 搜索高亮颜色。默认:#FFFF00
searchNavigateMatchColor?: string // 搜索导航高亮颜色。默认:#AAD280
searchMatchAlpha?: number // 搜索高亮透明度。默认:0.6
highlightAlpha?: number // 高亮元素透明度。默认:0.6
resizerColor?: string // 图片尺寸器颜色。默认:#4182D9
resizerSize?: number // 图片尺寸器大小。默认:5
marginIndicatorSize?: number // 页边距指示器长度。默认:35
marginIndicatorColor?: string // 页边距指示器颜色。默认:#BABABA
margins?: IMargin // 页面边距。默认:[100, 120, 100, 120]
pageMode?: PageMode // 纸张模式:连页、分页。默认:分页
tdPadding?: IPadding // 单元格内边距。默认:[0, 5, 5, 5]
defaultTrMinHeight?: number // 默认表格行最小高度。默认:42
defaultColMinWidth?: number // 默认表格列最小宽度(整体宽度足够时应用,否则会按比例缩小)。默认:40
defaultHyperlinkColor?: string // 默认超链接颜色。默认:#0000FF
header?: IHeader // 页眉信息。{top?:number; maxHeightRadio?:MaxHeightRatio;}
footer?: IFooter // 页脚信息。{bottom?:number; maxHeightRadio?:MaxHeightRatio;}
pageNumber?: IPageNumber // 页码信息。{bottom:number; size:number; font:string; color:string; rowFlex:RowFlex; format:string; numberType:NumberType;}
paperDirection?: PaperDirection // 纸张方向:纵向、横向
inactiveAlpha?: number // 正文内容失焦时透明度。默认值:0.6
historyMaxRecordCount?: number // 历史(撤销重做)最大记录次数。默认:100次
printPixelRatio?: number // 打印像素比率(值越大越清晰,但尺寸越大)。默认:3
maskMargin?: IMargin // 编辑器上的遮盖边距(如悬浮到编辑器上的菜单栏、底部工具栏)。默认:[0, 0, 0, 0]
letterClass?: string[] // 排版支持的字母类。默认:a-zA-Z。内置可选择的字母表类:LETTER_CLASS
contextMenuDisableKeys?: string[] // 禁用的右键菜单。默认:[]
scrollContainerSelector?: string // 滚动区域选择器。默认:document
wordBreak?: WordBreak // 单词与标点断行:BREAK_WORD首行不出现标点&单词不拆分、BREAK_ALL按字符宽度撑满后折行。默认:BREAK_WORD
watermark?: IWatermark // 水印信息。{data:string; color?:string; opacity?:number; size?:number; font?:string;}
control?: IControlOption // 控件信息。 {placeholderColor?:string; bracketColor?:string; prefix?:string; postfix?:string; borderWidth?: number; borderColor?: string;}
checkbox?: ICheckboxOption // 复选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;}
radio?: IRadioOption // 单选框信息。{width?:number; height?:number; gap?:number; lineWidth?:number; fillStyle?:string; strokeStyle?: string;}
cursor?: ICursorOption // 光标样式。{width?: number; color?: string; dragWidth?: number; dragColor?: string;}
title?: ITitleOption // 标题配置。{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;}
placeholder?: IPlaceholder // 编辑器空白占位文本
group?: IGroup // 成组配置。{opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean}
pageBreak?: IPageBreak // 分页符配置。{font?:string; fontSize?:number; lineDash?:number[];}
zone?: IZoneOption // 编辑器区域配置。{tipDisabled?:boolean;}
background?: IBackgroundOption // 背景配置。{color?:string; image?:string; size?:BackgroundSize; repeat?:BackgroundRepeat;}。默认:{color: '#FFFFFF'}
lineBreak?: ILineBreakOption // 换行符配置。{disabled?:boolean; color?:string; lineWidth?:number;}
separator?: ISeparatorOption // 分隔符配置。{lineWidth?:number; strokeStyle?:string;}
}interface IHeader {
top?: number // 距离页面顶部大小。默认:30
maxHeightRadio?: MaxHeightRatio // 占页面最大高度比。默认:HALF
disabled?: boolean // 是否禁用
}interface IPageNumber {
bottom?: number // 距离页面底部大小。默认:60
size?: number // 字体大小。默认:12
font?: string // 字体。默认:Microsoft YaHei
color?: string // 字体颜色。默认:#000000
rowFlex?: RowFlex // 行对齐方式。默认:CENTER
format?: string // 页码格式。默认:{pageNo}。示例:第{pageNo}页/共{pageCount}页
numberType?: NumberType // 数字类型。默认:ARABIC
disabled?: boolean // 是否禁用
startPageNo?: number // 起始页码。默认:1
fromPageNo?: number // 从第几页开始出现页码。默认:0
}interface IWatermark {
data: string // 文本。
color?: string // 颜色。默认:#AEB5C0
opacity?: number // 透明度。默认:0.3
size?: number // 字体大小。默认:200
font?: string // 字体。默认:Microsoft YaHei
}
@{
ViewData["Title"]="Index";
}
<h1>Index</h1>
<!--上传视频-->
<div>
<div>
<div>
<div>
<span class="">上传视频</span>
</div>
<input id="uploadvideofile" type="file" accept="video/mp4,video/quicktime" class="up-video" />
</div>
</div>
<div id="videoplayer" style="display: none"></div>
<div id="videooutput"></div>
<div>
<p style="color: #797979; margin-top: 5px; font-size: 12px;">视频格式必须为: mp4或mov。视频时长须在15秒以内,超出时长系统将自动截取前15秒内容。</p>
</div>
</div>
<script src="~/Scripts/ajax.1.5.2.js"></script>
<!--传视频相关-->
<script type="text/javascript">
$('#uploadvideofile').change(function() {
var file, videoURL, windowURL;
var filemaxsize=1024 * 1024 * 20; //200M
if (fileValid(this, filemaxsize, 'video')) {
file=this.files[0];
try {
var temp=ajax.upload_big(
"UpLoadBigFile/UploadVideo?opration=2222", //文件上传地址
"#uploadvideofile", //input=file 选择器
1024 * 1024, //切割文件大小
"*", //文件限制类型 mime类型
function(index) { //上传成功事件
//console.log("slice [ " + index + "] uploaded")
},
function(index, length) { //上传进度事件
//console.log(index + "/" + length);
//var ratio=(index / length) * 100.00 | 0.00;
//var progress=$(".progress > div");
//progress.css("width", ratio + "%");
//progress.attr("aria-valuenow", index);
//progress.attr("aria-valuemax", length);
//progress.children("span").text("" + ratio + "%");
},
function(index, length) { //超时处理事件
console.log(index + "/" + length);
}
);
} catch (e) {
console.log(e.name + " " + e.message);
}
videoURL=null;
windowURL=window.URL || window.webkitURL;
videoURL=windowURL.createObjectURL(file);
$('#videoplayer').html('<video src="' + videoURL + '" controls="controls"></video>');
setTimeout(function() { createIMG(); }, 800);
}
});
//验证上传文件大小和类型
/**
*
* param {this} value_ [获取input对象,一般为this]
* param {[number]} size_ [文件限制的大小,单位为kb]
* param {[string]} type_ [文件类别]
* param {[function]} callback [验证通过的回调]
*/
function fileValid(value_, size_, type_, callback) {
var file=value_.files[0];
var fileSize=(file.size / 1024).toFixed(0); //文件大小
var fileType=value_.value.substring(value_.value.lastIndexOf(".")); //文件类型
if (fileSize > size_) {
alert('视频过大,请选择小于200MB的视频!');
return false;
}
switch (type_) {
case 'video':
if (!fileType.match(/.mp4|.mov/i)) {
alert('请上传正确格式的视频!');
return false;
}
break;
default:
alert('参数设置不正确!');
return false;
break;
}
return true;
}
var createIMG=function() {
var scale=0.25,
video=$('#videoplayer').find('video')[0],
canvas=document.createElement("canvas"),
canvasFill=canvas.getContext('2d');
canvas.width=video.videoWidth * scale;
canvas.height=video.videoHeight * scale;
canvasFill.drawImage(video, 0, 0, canvas.width, canvas.height);
var src=canvas.toDataURL("image/jpeg");
$('#videooutput').html('<img id="imgSmallView" src="' + src + '" alt="预览图" />');
}
</script>
<script>
(function () {
//$("#uploadvideofile").change(function () {
// var temp=ajax.upload_big(
// "AjaxHandlers/UploadVideoHandler.ashx", //文件上传地址
// "#uploadvideofile", //input=file 选择器
// 1024 * 1024, //切割文件大小
// "*", //文件限制类型 mime类型
// function(index) { //上传成功事件
// //console.log("slice [ " + index + "] uploaded")
// },
// function(index, length) { //上传进度事件
// //console.log(index + "/" + length);
// //var ratio=(index / length) * 100.00 | 0.00;
// //var progress=$(".progress > div");
// //progress.css("width", ratio + "%");
// //progress.attr("aria-valuenow", index);
// //progress.attr("aria-valuemax", length);
// //progress.children("span").text("" + ratio + "%");
// },
// function(index, length) { //超时处理事件
// console.log(index + "/" + length);
// }
// );
// console.log(temp);
//});
})();
</script>
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using VideoDemo.Common;
using VideoDemo.Models;
namespace VideoDemo.Controllers
{
public class UploadBigFileController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
//[ValidateAntiForgeryToken]
public int UploadVideo([FromForm] VideoFileModel filemodel, [FromQuery]string opration)
{
try
{
int slicenum=FileHelper.UploadVideo(filemodel);
return slicenum;
//return RedirectToAction(nameof(Index));
}
catch(Exception e)
{
Console.Write(e.ToString());
return -1;
}
}
public ActionResult Details(int id)
{
return View();
}
public ActionResult Create()
{
return View();
}
// GET: UpLoadBigFileController/Edit/5
public ActionResult Edit(int id)
{
return View();
}
// POST: UpLoadBigFileController/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, IFormCollection collection)
{
try
{
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
}
}using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using VideoDemo.Models;
using Xabe.FFmpeg;
namespace VideoDemo.Common
{
public class FileHelper
{
private static string finalpath="";
public static int UploadVideo(VideoFileModel videoFile)
{
//前端传输是否为切割文件最后一个小文件
var isLast=videoFile.isLast;
//前端传输当前为第几次切割小文件
var count=videoFile.Count;
//获取前端处理过的传输文件名
string fileName=videoFile.Name;
//存储接受到的切割文件
if (videoFile.Files.Length <=0)
{
return -1;
}
IFormFile formFile=videoFile.Files;
//处理文件名称(去除.part*,还原真实文件名称)
string newFileName=fileName.Substring(0, fileName.LastIndexOf('.'));
//临时存储文件夹路径
string desPath=Directory.GetCurrentDirectory() + @"/uploads/slice/" + newFileName;
//判断指定目录是否存在临时存储文件夹,没有就创建
if (!System.IO.Directory.Exists(desPath))
{
//不存在就创建目录
System.IO.Directory.CreateDirectory(desPath);
}
//存储文件
using (var stream=new FileStream(desPath+"/"+ fileName, FileMode.Create))
{
formFile.CopyTo(stream);
}
//file.SaveAs("E:\\uploads\\slice\\" + newFileName + "\\" + fileName);
//判断是否为最后一次切割文件传输
if (isLast=="true")
{
//判断组合的文件是否存在
finalpath=Directory.GetCurrentDirectory() + @"/uploads/" + newFileName;
if (File.Exists(finalpath)) //如果文件存在
{
File.Delete(finalpath); //先删除,否则新文件就不能创建
}
//创建空的文件流
FileStream FileOut=new FileStream(finalpath, FileMode.CreateNew, FileAccess.ReadWrite);
BinaryWriter bw=new BinaryWriter(FileOut);
//获取临时存储目录下的所有切割文件
string[] allFile=Directory.GetFiles(desPath);
//将文件进行排序拼接
allFile=allFile.OrderBy(s=> int.Parse(Regex.Match(s, @"\d+$").Value)).ToArray();
for (int i=0; i < allFile.Length; i++)
{
FileStream FileIn=new FileStream(allFile[i], FileMode.Open);
BinaryReader br=new BinaryReader(FileIn);
byte[] data=new byte[1048576]; //流读取,缓存空间
int readLen=0; //每次实际读取的字节大小
readLen=br.Read(data, 0, data.Length);
bw.Write(data, 0, readLen);
//关闭输入流
FileIn.Close();
}
//关闭二进制写入
bw.Close();
FileOut.Close();
ClipVideo();
}
return int.Parse(count) + 1;
}
public static async void ClipVideo()
{
/**
* 支持视频格式:mpeg,mpg,avi,dat,mkv,rmvb,rm,mov.
*不支持:wmv
* **/
//FFmpeg.SetExecutablesPath("ffmpeg.exe");
//ffmpeg.exe的路径,控制台程序会在执行目录(....FFmpeg测试\bin\Debug)下找此文件,
//视频路径
string finalname=Path.GetFileNameWithoutExtension(finalpath);
string videoFilePath=finalpath; //"d:\\01.avi";
finalpath=finalpath.Replace(finalname, finalname + "15S");
//IMediaInfo videoFile=await FFmpeg.GetMediaInfo(videoFilePath);
//TimeSpan totaotp=videoFile.Duration;
//string totalTime=string.Format("{0:00}:{1:00}:{2:00}", (int)totaotp.TotalHours, totaotp.Minutes,totaotp.Seconds);
#region 自定义逻辑
//string sourceFileName=Path.GetFileName(upFile.get_FileName()); //取出上传的视频的文件名,进而取出该文件的扩展名
//string sourceFileName="02.avi";
//string flv_file=System.IO.Path.ChangeExtension("d:\\01.avi", ".flv");
//string Command=" -i \"" + FromName + "\" -y -ab 32 -ar 22050 -b 800000 -s 480*360 \"" + ExportName + "\""; //Flv格式
//转换视频为flv
//ffmpeg -i F:\01.wmv -ab 56 -ar 22050 -b 500 -r 15 -s 320x240 f:\test.flv
//视频截图,fileName视频地址,imgFile图片地址
//ffmpeg -i input.flv -y -f image2 -ss 10.11 -t 0.001 -s 240x180 catchimg.jpg;
//ImgstartInfo.Arguments=" -i " + fileName + " -y -f image2 -ss 2 -vframes 1 -s " + FlvImgSize + " " + flv_img;
//string Command=" -i \"test.wmv\" -y -ab 32 -ar 22050 -b 800000 -s 320*240 \"2.flv\"";
//string Command="E:\\FFmpeg\\ffmpeg.exe -i E:\\ClibDemo\\VideoPath\\admin\\a.wmv -y -ab 56 -ar 22050 -b 500 -r 15 -s 320*240 " ExportName;
//3.重新编码进行剪切
//ffmpeg -ss [start] -t [duration] -i [in].mp4 -c:v libx264 -c:a aac -strict experimental -b:a 98k [out].mp4
//相对来说比较精确,可是还是不是特别精确
//string Command=" -ss 00:00:00 -t 00:00:15 -i d:\\01.avi d:\\output.avi"; // ffmpeg -ss 00:00:10 -t 00:01:22 -i 五月天-突然好想你.mp3 out.mp3
string Command=" -ss 00:00:00 -t 00:00:15 -i " + videoFilePath + " " + finalpath;
string Command1= " -i d:\\01.avi -vf \"drawtext=fontfile=simhei.ttf: text='南通极客如皋张HC':x=w-tw-10:y=10:fontsize=28:fontcolor=red:shadowy=2\" d:\\output1.avi";
string Command2= " -i d:\\01.avi -vf \"drawtext=fontfile=simhei.ttf: text='南通极客QQ(827XXXXXX)':y=h-line_h-10:x=(w-mod(30*n\\,w+tw)):fontsize=34:fontcolor=yellow:shadowy=2\" d:\\output2.avi";
System.Diagnostics.Process p=new System.Diagnostics.Process();
//非控制台程序必须写完整路径
p.StartInfo.FileName=AppContext.BaseDirectory + ("/ffmpeg.exe");
p.StartInfo.Arguments=Command;
//p.StartInfo.Arguments=Command1;
//Asp.net 获取当前目录
p.StartInfo.WorkingDirectory=AppContext.BaseDirectory; //HttpContext.Current.Request.MapPath("~/"); //Environment.CurrentDirectory;
p.StartInfo.UseShellExecute=false;
p.StartInfo.RedirectStandardInput=true;
p.StartInfo.RedirectStandardOutput=true;
p.StartInfo.RedirectStandardError=true;
p.StartInfo.CreateNoWindow=false;
//开始执行
p.Start();
p.BeginErrorReadLine();
p.WaitForExit();
p.Close();
p.Dispose();
#endregion
//Console.WriteLine("时间长度:{0}", totalTime);
//Console.WriteLine("高度:{0}", videoFile.Height);
//Console.WriteLine("宽度:{0}", videoFile.Width);
//Console.WriteLine("数据速率:{0}", videoFile.VideoBitRate);
//Console.WriteLine("数据格式:{0}", videoFile.VideoFormat);
//Console.WriteLine("比特率:{0}", videoFile.BitRate);
//Console.WriteLine("文件路径:{0}", videoFile.Path);
Console.ReadKey();
}
}
}ainless Lab 是 Elasticsearch 7.13 引入的实验性功能,是一个交互式代码编辑器,可以实时测试和调试 Painless 脚本。
本文展开解读 Painless Lab 如何应用于企业级实战开发中的脚本调试环节!
Painless Lab是一个交互式的测试版代码编辑器,用于实时测试和调试Painless脚本。
咱们可以通过打开主菜单,点击开发工具,然后选择 Painless Lab 来访问它。
如下图所示,左侧是:脚本输入区域。右侧由三部分组成:
一句话:Painless Lab 可以实时测试和调试 Painless 脚本。
Painless Lab 允许我们创建 Kibana 运行时字段(runtime fields)、处理重新索引的数据(reindex)、定义复杂的 Watcher 条件(付费功能),并在其他上下文中处理数据。
下面的 Context 部分展开就是 Painless Lab 的核心功能区域。
三种类型进一步展开:
进一步再展开解读。
https://www.elastic.co/guide/en/elasticsearch/painless/8.11/painless-execute-api.html#_contexts
上下文描述painless_test默认上下文,如果没有指定其他上下文则使用此上下文。用于通用脚本测试,例如调试和验证脚本逻辑。filter将脚本视为在脚本查询中运行。用于过滤数据。score将脚本视为在 function_score 查询中的 script_score 函数中运行。用于评分数据。
默认上下文,如果没有指定其他上下文则使用此上下文。用于通用脚本测试,例如调试和验证脚本逻辑。
将脚本视为在脚本查询中运行。用于过滤数据。
将脚本视为在 function_score 查询中的 script_score 函数中运行。用于评分数据。
我们逐一详尽展开解读,确保大家跟着过一遍,就能学得会!
Basic 上下文允许我们独立测试脚本逻辑,并将结果转换为字符串输出。
样例数据可以放到 params 中作为输入。
实战举例如下:
对于 Ingest pipeline 脚本,参考官方示例,由于脚本是在 Ingest Pipeline 中处理数据的,并且没有涉及到查询过滤 filter 或评分 score,因此 Basic 上下文是最合适的选择。
https://www.elastic.co/guide/en/elasticsearch/reference/current/script-processor.html
如上图所示,我们在 parameters 中输入如下数据:
{
"ctx": {
"env": "小米-笔记本-电脑-雷军"
},
"params": {
"delimiter": "-",
"position": 1
}
}
我们在左侧输入如下的 painless script 脚本,如下所示:
// 获取 ctx 数据
def ctx=params.ctx;
// 获取参数
def delimiter=params.params.delimiter;
def position=params.params.position;
// 执行脚本逻辑
String[] envSplit=ctx.env.splitOnToken(delimiter);
ArrayList tags=new ArrayList();
//tags.add(envSplit[position].trim());
for (def tag : envSplit) {
tags.add(tag.trim());
}
ctx.tags=tags;
// 返回结果以供调试
return ctx.tags;
执行结果如下图右侧 Output 所示。
上述脚本实现的核心功能就是:以分隔符截断字符串,形成独立字符串,插入到 tags 集合中。
这样调试过之后,再微调一下就可以应用到 ingest pipeline 中。
区别于刚才的逻辑,这里需要我们先创建索引,然后基于我们构造的索引数据进行展开 filter 过滤检索。
POST /hockey/_doc/1
{
"first": "johnny",
"last": "gaudreau",
"goals": [9, 27, 1],
"assists": [17, 46, 0],
"gp": [26, 82, 1]
}
POST /hockey/_doc/2
{
"first": "john",
"last": "doe",
"goals": [2, 3, 4],
"assists": [1, 2, 3],
"gp": [4, 5, 6]
}
上述索引必须构建,否则会报错如下图所示。
错误原因可能是:索引不存在或者Mapping 不存在。
正确的执行步骤如下所示:
结合上面三个步骤以及左侧的脚本,主要验证左侧脚本正确与否。注意:返回值必须是 Bool 类型。执行结果如下:
最终结合上述调试成功的脚本,整合到 script query 检索语句中,就能得到满足用户预期的结果数据。
POST /hockey/_search
{
"query": {
"bool": {
"filter": {
"script": {
"script": {
"lang": "painless",
"source": """
// 获取 goals 字段的值
def goals=doc['goals'];
// 初始化总和变量
int sum=0;
// 遍历 goals 数组并计算总和
for (int i=0; i < goals.size(); i++) {
sum +=goals.get(i);
}
// 输出调试信息
//Debug.explain(sum);
// 返回 true 以匹配所有文档,仅用于调试目的
return sum>10;
"""
}
}
}
}
}
}
在 Elasticsearch 中,score 类型调试上下文用于在 function_score 查询中的 script_score 函数中运行脚本。
该方式允许用户编写脚本来动态计算文档的评分,从而影响搜索结果的排序。
假设我们有一个包含产品信息的索引 products,每个文档包含以下字段:
我们希望根据价格和评分来动态计算每个产品的分数,具体规则如下:
POST /products/_doc/1
{
"name": "Product A",
"price": 100,
"rating": 4.5
}
POST /products/_doc/2
{
"name": "Product B",
"price": 200,
"rating": 4.0
}
POST /products/_doc/3
{
"name": "Product C",
"price": 150,
"rating": 3.5
}
我们将编写一个 function_score 查询,使用 Painless 脚本来计算每个文档的分数,并根据计算结果排序。
核心逻辑:
在 Painless Lab 中,可以使用类似的脚本来调试和验证评分逻辑:
{
"price": 100,
"rating": 4.5
}
左侧脚本部分
// 获取参数值
long price=params.price;
double rating=params.rating;
// 检查字段值是否存在
if (price==0 || rating==0) {
// 如果任一字段值为 0,则返回默认分数(例如 0)
return 0.0;
}
// 自定义评分逻辑
double score=(1 / (float)price) * rating;
// 返回评分结果
return score;
执行结果如下所示:
上述脚本通过使用 score 上下文中的 script_score 函数,可以根据自定义逻辑动态计算文档的分数,从而影响搜索结果的排序。
这在需要根据复杂规则排序搜索结果时非常有用。
通过在 Painless Lab 中调试和验证上述脚本,可以确保评分逻辑的正确性和有效性。
进而,可以组合写出如下的评分脚本检索语句。
POST /products/_search
{
"query": {
"function_score": {
"query": {
"match_all": {}
},
"script_score": {
"script": {
"source": """
long price=doc['price'].value;
double rating=doc['rating'].value;
// 自定义评分逻辑
double score=(1 / (float)price) * rating;
return score;
"""
}
},
"boost_mode": "replace" // 使用脚本计算的分数替换原始分数
}
}
}
实现核心:根据自定义逻辑计算分数:score=(1 / price) * rating。价格越低,评分越高,分数越高。
boost_mode: 设置为 replace,使用脚本计算的分数替换原始分数。
Kibana Painless Lab 是 Elasticsearch 7.13 引入的实验性功能,为开发者提供交互式代码编辑器,用于实时测试和调试 Painless 脚本。
通过 painless_test、filter 和 score 上下文三种测试方式,开发者可以创建和调试 Kibana 运行时字段、处理重新索引的数据、定义复杂的 Watcher 条件,并根据复杂规则动态计算文档分数,提高脚本开发和优化效率。
作者:铭毅天下
来源-微信公众号:铭毅天下Elasticsearch
出处:https://mp.weixin.qq.com/s/x9FrVCyrmpvJsc7pNGBm2g
*请认真填写需求信息,我们会在24小时内与您取得联系。